以前我在文章《WCF入門(六)——回調》中介紹了在WCF中通過回調的方式實現雙工通信,然而在回調的時候是非常容易出現死鎖的,本文就簡單的介紹幾種常見的死鎖的方式和解決方案。
一、服務器端死鎖
對於如下服務:
[ServiceContract(CallbackContract = typeof(INotify))]
public class DownloadService
{
[OperationContract]
public void Download()
{
//開始下載操作
//.....
//通知下載完成
var callback = OperationContext.Current.GetCallbackChannel<INotify>();
callback.DownloadComplete();
}
}
interface Inotify
{
[OperationContract]
void DownloadComplete();
}
首先我們用一個控制台程序作為客戶端,代碼如下:
class Program
{
static void Main(string[] args)
{
var context = new System.ServiceModel.InstanceContext(new CallBack());
var client = new DownloadServiceClient(context);
client.Download();
}
}
class CallBack : DownloadServiceCallback
{
public void DownloadComplete()
{
Console.WriteLine("finished");
}
}
當運行這個程序時,會出現如下異常:
未經處理的異常: System.ServiceModel.FaultException`1[System.ServiceModel.ExceptionDetail]: 此操作將死鎖,因為在當前郵件完成處理以前無法收到答復。如果要允許無序的郵件處理,則在 ServiceBehaviorAttribute 上指定可重輸入的或多個 ConcurrencyMode。
這個異常已經說得非常清楚了:當前服務不支持並發,在處理Download函數的時候信道是串行的,返回消息的時候必須先返回Download函數的處理消息,再返回DownloadComplete回調獲取回調的返回結果;但Download函數本身又在等待DownloadComplete函數返回,從而形成了一個死鎖。
這個異常同時也告訴了我們一個修改方案:修改服務的ServiceBehavior的ConcurrencyMode為Reentrant或Multiple即可。
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
public class DownloadService
實際上,這個問題還有另一種解決方案:如果無需等待回調完成,可以把回調函數設置為OneWay的方式;這樣Download函數就不等待回調函數的返回結果了,不會因為互相等待而導致死鎖。
interface Inotify
{
[OperationContract(IsOneWay=true)]
void DownloadComplete();
}
二、客戶端死鎖
由於系統自己能分析出服務器端的死鎖,服務器端的死鎖還是比較容易發現和處理的。但客戶端的死鎖就不是那么容易發現了。
經過前面的處理后,服務器端死鎖問題已經解決了,客戶端可以順利處理了。這次我們把客戶端代碼放到WinForm版本的程序中,放在UI線程中處理。
private void button1_Click(object sender, EventArgs e)
{
var context = new System.ServiceModel.InstanceContext(new CallBack());
var client = new DownloadServiceClient(context);
client.Download();
}
當運行上述代碼時,可以發現:點擊按鈕后,窗口就無響應了。由於此時沒有任何錯誤提示信息,我就直接給出錯誤原因了:WCF的回調函數默認是在UI線程中執行,因此就會出現Download函數等待DownloadComplete回調執行完后才返回,而DownloadComplete回調又因為Download函數又等待着Download函數返回釋放UI線程才能執行,這樣又形成了一個死鎖。而控制台程序沒有UI線程,不會出現這種死鎖。
對於這種死鎖,根本的方案就是修改回調回調函數的通知線程,將其改為在非UI線程中執行。WCF中可以通過在客戶端回調函數類中的CallbackBehaviorAttribute中控制這一行為
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)]
class CallBack : DownloadServiceCallback
我在這里還設置了CallbackBehaviorAttribute的ConcurrencyMode參數,這個參數在本例中是不必要的,但我還是習慣性的將其帶上了,至於它在什么時候會用到,請讀者朋友們自行分析。