在上一次的文章中,我們說到了如何設計一個ASP.NET Web Service來處理長串連請求。很多人對此就提出了問題,如何hold住請求讓它30秒不斷開了?這其實很簡單,只需要Sleep()一下就可以了:
Thread.Sleep(30 * 1000);
然而問題是,我們不是要等30秒然後看看是否有事件需要返回,而是在這30秒內隨時有事件隨時返回。因此,我們需要一套機制來在等待的過程中檢查是否有事件發生了。
Monitor模型
在.NET裡面,大家最熟悉的線程同步模型應該就是Monitor模型了。沒聽說過?就是C#的那個lock關鍵字,實際上它編譯出來就是一對Monitor.Enter()和Monitor.Exit()。
通過lock命令,我們可以針對一個對象建立一個臨界區,代碼執行到臨界區入口時必須擷取到該對象的鎖才能執行下去,並且在臨界區的出口釋放該鎖。然而這種模型不太適用於解決我們的問題,因為我們需要等待一個事件,如果使用lock來等待的話,那就是說要先在Web Service外部把對象鎖上,然後等事件觸發了就解鎖,這時候Web Service才順利進入臨界地區。
事實上,要進行這類型的阻塞,還有一個更好的選擇,那就是Mutex。
Mutex模型
Mutex,也就是mutual exclusive的縮寫,“互斥”的意思。Mutex是如何運作的?這有點像是銀行的排隊叫號系統,所有等待服務的人都坐在大廳裡等候(wait)被叫,當一個服務視窗空閑時它就會發出一個訊號(signal)來通知下一位等候服務的人。總之,所有執行wait指令的線程都在等候,而每一個signal能夠讓一個線程結束等候繼續執行。
在.NET裡面,wait和signal這兩個操作分別對應Mutex.WaitOne()和Mutex.ReleaseMutex()這兩個方法。我們可以讓Web Service的線程使用Mutex.WaitOne()進入等候狀態,而在事件發生時使用Mutex.ReleaseMutex()來通知Web Service線程。因為必須在Mutex.ReleaseMutex()發生後Mutex.WaitOne()才可能繼續執行下去,因此能夠執行下去就證明必然有事件發生了並且調用了Mutex.ReleaseMutext(),這時候就可以放心地去讀取事件訊息了。
簡單樣本
在選定使用Mutex模型後,我們來編寫一個簡單的樣本。首先,我們要在WebService衍生類別內定義一個Mutex,還有一個代表訊息的字串。
Mutex mutex = new Mutex();
string message;
然後,我們定義兩個WebMethod。為了把問題簡單化,我們選用上一篇文章中開頭所說的兩個函數簽名,也就說只能在一個Web Service內自己發自己收,沒有發送目標的概念,也沒有逾時的概念,還沒有可靠性設計。同時,我們將Message類型替換為一般字元串,以便於我們測試。
我們先編寫發送訊息的函數:
public void Send(string message) {
this.message = message;
this.mutex.ReleaseMutex();
}
在這個發送函數裡,首先我們把訊息放進了類內全域的變數中,然後讓全域的Mutex類釋放一個signal。這時候,如果有線程在等待,它可以馬上執行下去。如果此時沒有線程在等待,那麼下一個wait的線程執行到該阻塞的地方就能夠不受阻塞繼續執行下去。
現在我們來編寫接收訊息的函數:
public string Wait() {
this.mutex.WaitOne();
return this.message;
}
接收函數一開始就進入wait狀態。在得到signal後,需要做的事情就是把全域的訊息返回給用戶端。
親身體驗
最後,我們可以通過ASP.NET Web Service本身支援的Web測試介面來測試一下我們的代碼。我們開兩個瀏覽器視窗,一個進入Send()調用,一個進入Wait()調用。然後我們按照如下方法來測試:
- 首先執行Send("Hello"),然後執行Wait()。這時候你可以馬上看到"Hello"。
- 首先執行Wait(),讓它等待返回,這時候執行Send("Hello")。隨後你可以看到Wait()那段返回"Hello"了。
- 按如下順序執行:Send("Hello");Wait();Send("World");Wait();
- 按如下順序執行:Send("Hello");Send("World");Wait();Wait();
- 按如下順序執行:Wait();Wait();Send("Hello");Send("World");
- 按如下順序執行:Wait();Send("Hello");Wait();Send("World");
你會發現這樣一些奇怪的結果:第3個測試返回的是"World"和"World"。第5個測試先返回"Hello"的並不一定是先執行的那個Wait()線程。後者在某些情況下不是什麼問題,特別是長串連中一般之後一個Wait()線程在等待中,所以我們可以不管。而前者,則是因為沒有訊息佇列所造成的,我們只有長度為1的訊息視窗,所以只能緩衝最後一個訊息。這個問題我們將在下一篇文章中解決。
小結
在本文中,我們看到了不同的線程同步模型的差異。Monitor模型的lock本質上是一個Semaphore,也就是一個不能連續signal的Mutex,一個signal發出去後必須被一個wait接收了才能進行下一次的signal。同時,Semaphore也限制了signal和wait必須在同一個線程內成對執行,而Mutex則沒有此限制。雖然.NET是針對Monitor模型最佳化的,但在我們的需求當中,只能通過Mutex模型來解決。
接著,我們便寫了一個小小的消協發送與接收函數,實現了我們想要的阻塞式Web Service。同時我們也看到了沒有訊息佇列造成的問題,因此確定接下來我們要做一個訊息佇列。