C#中的執行緒(二) 執行緒同步基礎
1.同步要領
下面的表格列展了.NET對協調或同步執行緒動作的可用的工具:
簡易阻止方法
構成 |
目的 |
Sleep |
阻止給定的時間週期 |
Join |
等待另一個執行緒完成 |
鎖系統
構成 |
目的 |
跨程序? |
速度 |
lock |
確保只有一個執行緒訪問某個資源或某段程式碼。 |
否 |
快 |
Mutex |
確保只有一個執行緒訪問某個資源或某段程式碼。可被用於防止一個程式的多個例項同時執行。 |
是 |
中等 |
Semaphore |
確保不超過指定數目的執行緒訪問某個資源或某段程式碼。 |
是 |
中等 |
(同步的情況下也提夠自動鎖。)
訊號系統
構成 |
目的 |
跨程序? |
速度 |
EventWaitHandle |
允許執行緒等待直到它受到了另一個執行緒發出訊號。 |
是 |
中等 |
Wait 和 Pulse* |
允許一個執行緒等待直到自定義阻止條件得到滿足。 |
否 |
中等 |
非阻止同步系統*
構成 |
目的 |
跨程序? |
速度 |
Interlocked* |
完成簡單的非阻止原子操作。 |
是(記憶體共享情況下) |
非常快 |
volatile* |
允許安全的非阻止在鎖之外使用個別欄位。 |
非常快 |
* 代表頁面將轉到第四部分
1.1 阻止 (Blocking)
當一個執行緒通過上面所列的方式處於等待或暫停的狀態,被稱為被阻止。一旦被阻止,執行緒立刻放棄它被分配的
CPU時間,將它的ThreadState屬性新增為WaitSleepJoin狀態,不在安排時間直到停止阻止。停止阻止在任意四種
情況下發生(關掉電腦的電源可不算!):
- 阻止的條件已得到滿足
- 操作超時(如果timeout被指定了)
- 通過Thread.Interrupt中斷了
- 通過Thread.Abort放棄了
當執行緒通過(不建議)Suspend 方法暫停,不認為是被阻止了。
1.2 休眠 和 輪詢
呼叫Thread.Sleep阻止當前的執行緒指定的時間(或者直到中斷):
1 2 3 4 5 6 |
static void Main() {
Thread.Sleep (0); // 釋放CPU時間片
Thread.Sleep (1000); // 休眠1000毫秒
Thread.Sleep (TimeSpan.FromHours (1)); // 休眠1小時
Thread.Sleep (Timeout.Infinite); // 休眠直到中斷
}
|
更確切地說,Thread.Sleep放棄了佔用CPU,請求不在被分配時間直到給定的時間經過。Thread.Sleep(0)放棄
CPU的時間剛剛夠其它在時間片佇列裡的活動執行緒(如果有的話)被執行。
Thread.Sleep在阻止方法中是唯一的暫停汲取Windows Forms程式的Windows訊息的方法,或COM環境中用於
單元模式。這在Windows Forms程式中是一個很大的問題,任何對主UI執行緒的阻止都將使程式失去相應。因此一般避
免這樣使用,無論資訊汲取是否被“技術地”暫定與否。由COM遺留下來的宿主環境更為複雜,在一些時候它決定停止,
而卻保持資訊的汲取存活。微軟的 Chris Brumm 在他的部落格中討論這個問題。(搜尋: 'COM "Chris Brumme"')
執行緒類同時也提供了一個SpinWait方法,它使用輪詢CPU而非放棄CPU時間的方式,保持給定的迭代次數進行“無用
地繁忙”。50迭代可能等同於停頓大約一微秒,雖然這將取決於CPU的速度和負載。從技術上講,SpinWait並不是一個阻
止的方法:一個處於spin-waiting的執行緒的ThreadState不是WaitSleepJoin狀態,並且也不會被其它的執行緒過早的中斷
(Interrupt)。SpinWait很少被使用,它的作用是等待一個在極短時間(可能小於一微秒)內可準備好的可預期的資源,
而不用呼叫Sleep方法阻止執行緒而浪費CPU時間。不過,這種技術的優勢只有在多處理器計算機:對單一處理器的電腦,
直到輪詢的執行緒結束了它的時間片之前,一個資源沒有機會改變狀態,這有違它的初衷。並且呼叫SpinWait經常會花費較
長的時間這本身就浪費了CPU時間。
1.3 阻止 vs. 輪詢
執行緒可以等待某個確定的條件來明確輪詢使用一個輪詢的方式,比如:
1 |
while (!proceed);
|
或者:
1 |
while (DateTime.Now < nextStartTime);
|
這是非常浪費CPU時間的:對於CLR和作業系統而言,執行緒進行了一個重要的計算,所以分配了相應的資源!在這種狀態
下的輪詢執行緒不算是阻止,不像一個執行緒等待一個EventWaitHandle(一般使用這樣的訊號任務來構建)。
阻止和輪詢組合使用可以產生一些變換:
1 |
while (!proceed) Thread.Sleep (x); // "輪詢休眠!"
|
x越大,CPU效率越高,折中方案是增大潛伏時間,任何20ms的花費是微不足道的,除非迴圈中的條件是極其複雜的。
除了稍有延遲,這種輪詢和休眠的方式可以結合的非常好。(但有併發問題,在第四部分討論)可能它最大的用處在於
程式設計師可以放棄使用複雜的訊號結構 來工作了。
1.4 使用Join等待一個執行緒完成
你可以通過Join方法阻止執行緒直到另一個執行緒結束:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class JoinDemo {
static void Main() {
Thread t = new Thread ( delegate () { Console.ReadLine();});
t.Start();
t.Join(); // 等待直到執行緒完成
Console.WriteLine ( "Thread t's ReadLine complete!" );
}
}
|
Join方法也接收一個使用毫秒或用TimeSpan類的超時引數,當Join超時是返回false,如果執行緒已終止,則返回true 。
Join所帶的超時引數非常像Sleep方法,實際上下面兩行程式碼幾乎差不多:
1 2 3 |
Thread.Sleep (1000);
Thread.CurrentThread.Join (1000);
|
(他們的區別明顯在於單執行緒的應用程式域與COM互操作性,源於先前描述Windows資訊汲取部分:在阻止時,Join
保持資訊汲取,Sleep暫停資訊汲取。)
2. 鎖和執行緒安全
鎖實現互斥的訪問,被用於確保在同一時刻只有一個執行緒可以進入特殊的程式碼片段,考慮下面的類:
1 2 3 4 5 6 7 8 |
class ThreadUnsafe {
static int val1, val2;
static void Go() {
if (val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
}
|
這不是執行緒安全的:如果Go方法被兩個執行緒同時呼叫,可能會得到在某個執行緒中除數為零的錯誤,因為val2可能被一個
執行緒設定為零,而另一個執行緒剛好執行到if和Console.WriteLine語句。
下面用lock來修正這個問題:
1 2 3 4 5 6 7 8 9 10 11 12 |
class ThreadSafe {
static object locker = new object ();
static int val1, val2;
static void Go() {
lock (locker) {
if (val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
}
}
|
在同一時刻只有一個執行緒可以鎖定同步物件(在這裡是locker),任何競爭的的其它執行緒都將被阻止,直到這個鎖被釋放。如果有大於一個的執行緒競爭這個 鎖,那麼他們將形成稱為“就緒佇列”的佇列,以先到先得的方式授權鎖。互斥鎖有時被稱之對由鎖所保護的內容強迫序列化訪問,因為一個執行緒的訪問不能與另一 個重疊。在這個例子中,我們保護了Go方法的邏輯,以及val1 和val2欄位的邏輯。
一個等候競爭鎖的執行緒被阻止將在ThreadState上為WaitSleepJoin狀態。稍後我們將討論一個執行緒通過另一個執行緒呼叫
Interrupt或Abort方法來強制地被釋放。這是一個相當高效率的技術可以被用於結束工作執行緒。
C#的lock 語句實際上是呼叫Monitor.Enter和Monitor.Exit,中間夾雜try-finally語句的簡略版,下面是實際發生在之前例
子中的Go方法:
1 2 3 4 5 6 7 8 |
Monitor.Enter (locker);
try {
if (val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
finally { Monitor.Exit (locker);
}
|
在同一個物件上,在呼叫第一個之前Monitor.Enter而先呼叫了Monitor.Exit將引發異常。
Monitor 也提供了TryEnter方法來實現一個超時功能——也用毫秒或TimeSpan,如果獲得了鎖返回true,反之沒有獲得返回false,因為超時了。TryEnter也可以沒有超時引數,“測試”一下鎖,如果鎖不能被獲取的話就立刻超時。
2.1 選擇同步物件
任何對所有有關係的執行緒都可見的物件都可以作為同步物件,但要服從一個硬性規定:它必須是引用型別。也強烈建議同步物件最好私有在類裡面(比如一個私有實 例欄位)防止無意間從外部鎖定相同的物件。服從這些規則,同步物件可以兼物件和保護兩種作用。比如下面List :
1 2 3 4 5 6 7 8 9 10 11 |
class ThreadSafe {
List < string > list = new List < string >();
void Test() {
lock (list) {
list.Add ( "Item 1" );
...
|
一個專門欄位是常用的(如在先前的例子中的locker) , 因為它可以精確控制鎖的範圍和粒度。用物件或類本身的型別作為一個同步物件,即:
lock (this) { ... }
或:
lock (typeof (Widget)) { ... } // 保護訪問靜態
是不好的,因為這潛在的可以在公共範圍訪問這些物件。
鎖並沒有以任何方式阻止對同步物件本身的訪問,換言之,x.ToString()不會由於另一個執行緒呼叫lock(x) 而被阻止,兩者都要呼叫ock(x) 來完成阻止工作。
2.2 巢狀鎖定
執行緒可以重複鎖定相同的物件,可以通過多次呼叫Monitor.Enter或lock語句來實現。當對應編號的Monitor.Exit被呼叫或最外面的lock語句完成後,物件那一刻被解鎖。這就允許最簡單的語法實現一個方法的鎖呼叫另一個鎖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static object x = new object ();
static void Main() {
lock (x) {
Console.WriteLine ( "I have the lock" );
Nest();
Console.WriteLine ( "I still have the lock" );
}
在這鎖被釋放
}
static void Nest() {
lock (x) {
...
} 釋放了鎖?沒有完全釋放!
}
|
執行緒只能在最開始的鎖或最外面的鎖時被阻止。
2.3 何時進行鎖定
作為一項基本規則,任何和多執行緒有關的會進行讀和寫的欄位應當加鎖。甚至是極平常的事情——單一欄位的賦值操作,都必須考慮到同步問題。在下面的例子中Increment和Assign 都不是執行緒安全的:
1 2 3 4 5 |
class ThreadUnsafe {
static int x;
static void Increment() { x++; }
static void Assign() { x = 123; }
}
|
下面是Increment 和 Assign 執行緒安全的版本:
1 2 3 4 5 6 7 |
class ThreadUnsafe {
static object locker = new object ();
static int x;
static void Increment() { lock (locker) x++; }
static void Assign() { lock (locker) x = 123; }
}
|
作為鎖定另一個選擇,在一些簡單的情況下,你可以使用非阻止同步,在第四部分討論(即使像這樣的語句需要同步的原因)。
2.4 鎖和原子操作
如果有很多變數在一些鎖中總是進行讀和寫的操作,那麼你可以稱之為原子操作。我們假設x 和 y不停地讀和賦值,他們在鎖內通過
locker鎖定:
lock (locker) { if (x != 0) y /= x; }
你可以認為x 和 y 通過原子的方式訪問,因為程式碼段沒有被其它的執行緒分開 或 搶佔,別的執行緒改變x 和 y是無效的輸出,你永遠不會得到除數為零的錯誤,保證了x 和 y總是被相同的排他鎖訪問。
2.5 效能考量
鎖定本身是非常快的,一個鎖在沒有堵塞的情況下一般只需幾十納秒(十億分之一秒)。如果發生堵塞,任務切換帶來的開銷接近於數微秒(百萬分之一秒)的範圍 內,儘管線上程重組實際的安排時間之前它可能花費數毫秒(千分之一秒)。而相反,與此相形見絀的是該使用鎖而沒使用的結果就是帶來數小時的時間,甚至超 時。如果耗盡併發,鎖定會帶來反作用,死鎖和爭用鎖,耗盡併發由於太多的程式碼被放置到鎖語句中了,引起其它執行緒不必要的被阻止。死鎖是兩執行緒彼此等待被鎖 定的內容,導致兩者都無法繼續下去。爭用鎖是兩個執行緒任一個都可以鎖定某個內容,如果“錯誤”的執行緒獲取了鎖,則導致程式錯誤。
對於太多的同步物件死鎖是非常容易出現的症狀,一個好的規則是開始於較少的鎖,在一個可信的情況下涉及過多的阻止出現時,增加鎖的粒度。
2.6 執行緒安全
執行緒安全的程式碼是指在面對任何多執行緒情況下,這程式碼都沒有不確定的因素。執行緒安全首先完成鎖,然後減少線上程間互動的可能性。
一個執行緒安全的方法,在任何情況下可以可重入式呼叫。通用型別在它們中很少是執行緒安全的,原因如下:
-
- 完全執行緒安全的開發是重要的,尤其是一個型別有很多欄位(在任意多執行緒上下文中每個欄位都有潛在的互動作用)的情況下。
- 執行緒安全帶來效能損失(要付出的,在某種程度上無論與否型別是否被用於多執行緒)。
- 一個執行緒安全型別不一定能使程式使用執行緒安全,有時參與工作後者可使前者變得冗餘。
因此執行緒安全經常只在需要實現的地方來實現,為了處理一個特定的多執行緒情況。
不過,有一些方法來“欺騙”,有龐大和複雜的類安全地執行在多執行緒環境中。一種是犧牲粒度包含大段的程式碼——甚至在排他鎖中訪問全域性物件,迫使在更高的級 別上實現序列化訪問。這一策略也很關鍵,讓非執行緒安全的物件用於執行緒安全程式碼中,避免了相同的互斥鎖被用於保護對在非執行緒安全物件的所有的屬性、方法和字 段的訪問。
原始型別除外,很少的.NET framework型別例項相比於併發的只讀訪問,是執行緒安全的。責任在開放人員實現執行緒安全代表性地使用互斥鎖。
另一個方式欺騙是通過最小化共享資料來最小化執行緒互動。這是一個很好的途徑,被暗中地用於“弱狀態”的中間層程式和web伺服器。自多個客戶端請求同時到 達,每個請求來自它自己的執行緒(效力於ASP.NET,Web伺服器或者遠端體系結構),這意味著它們呼叫的方法一定是執行緒安全的。弱狀態設計(因伸縮性 好而流行)本質上限制了互動的能力,因此類不能夠在每個請求間持久保留資料。執行緒互動僅限於可以被選擇建立的靜態欄位,多半是在記憶體裡快取常用資料和提供 基礎設施服務,例如認證和稽核。
2.7執行緒安全與.NET Framework型別
鎖定可被用於將非執行緒安全的程式碼轉換成執行緒安全的程式碼。比較好的例子是在.NET framework方面,幾乎所有非基本型別的例項都不是執行緒安全的,而如果所有的訪問給定的物件都通過鎖進行了保護的話,他們可以被用於多執行緒程式碼中。 看這個例子,兩個執行緒同時為相同的List增加條目,然後列舉它:.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ThreadSafe {
static List < string > list = new List < string >();
static void Main() {
new Thread (AddItems).Start();
new Thread (AddItems).Start();
}
static void AddItems() {
for ( int i = 0; i < 100; i++)
lock (list)list.Add ( "Item " + list.Count);
string [] items;
lock (list) items = list.ToArray();
foreach ( string s in items) Console.WriteLine (s);
}
}
|
在這種情況下,我們鎖定了list物件本身,這個簡單的方案是很好的。如果我們有兩個相關的list,也許我們就要鎖定一個共同的目標——單獨的一個字 段,如果沒有其它的list出現,顯然鎖定它自己是明智的選擇。列舉.NET的集合也不是執行緒安全的,在列舉的時候另一個執行緒改動list的話,會丟擲異 常。為了不直接鎖定列舉過程,在這個例子中,我們首先將專案複製到陣列當中,這就避免了固定住鎖因為我們在列舉過程中有潛在的耗時。
這裡的一個有趣的假設:想象如果List實際上為執行緒安全的,如何解決呢?程式碼會很少!舉例說明,我們說我們要增加一個專案到我們假象的執行緒安全的list裡,如下:
if (!myList.Contains (newItem)) myList.Add (newItem);
無論與否list是否為執行緒安全的,這個語句顯然不是!(因此,可以說完全執行緒安全的通用集合類是基本不存在的。.net4.0中,微軟提供了一組執行緒安全的並行集合類,但是都是特殊的經過處理過的,訪問方式都經過了限定。),上面的語句要實現執行緒安全,整個if語句必須放到一個鎖中,用來保護搶佔在判斷有無和增加新的之間。上述的鎖需要用於任何我們需要修改list的地方,比如下面的語句需要被同樣的鎖包括住:
myList.Clear();
來保證它沒有搶佔之前的語句,換言之,我們必須鎖定差不多所有非執行緒安全的集合類們。內建的執行緒安全,顯而易見是浪費時間!
在寫自定義元件的時候,你可能會反對這個觀點——為什麼建造執行緒安全讓它容易的結果會變的多餘呢 ?
有一個爭論:在一個物件包上自定義的鎖僅在所有並行的執行緒知道、並使用這個鎖的時候才能工作,而如果鎖物件在更大的範圍內的時候,這個鎖物件可能不在這個 鎖範圍內。最糟糕的情況是靜態成員在公共型別中出現了,比如,想象靜態結構在DateTime上,DateTime.Now不是執行緒安全的,當有2個併發 的呼叫可帶來錯亂的輸出或異常,補救方式是在其外進行鎖定,可能鎖定它的型別本身—— lock(typeof(DateTime))來圈住呼叫DateTime.Now,這會工作的,但只有所有的程式設計師同意這樣做的時候。然而這並靠不住, 鎖定一個型別被認為是一件非常不好的事情。由於這些理由,DateTime上的靜態成員是保證執行緒安全的,這是一個遍及.NET framework一個普遍模式——靜態成員是執行緒安全的,而一個例項成員則不是。從這個模式也能在寫自定義型別時得到一些體會,不要建立一個不能執行緒安全的難題!
當寫公用元件的時候,好的習慣是不要忘記了執行緒安全,這意味著要單獨小心處理那些在其中或公共的靜態成員。
3. Interrupt 和 Abort
一個被阻止的執行緒可以通過兩種方式被提前釋放:
- 通過 Thread.Interrupt
- 通過 Thread.Abort
這必須通過另外活動的執行緒實現,等待的執行緒是沒有能力對它的被阻止狀態做任何事情的。
3.1 Interrupt方法
在一個被阻止的執行緒上呼叫Interrupt 方法,將強迫釋放它,丟擲ThreadInterruptedException異常,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Program {
static void Main() {
Thread t = new Thread ( delegate () {
try {
Thread.Sleep (Timeout.Infinite);
}
catch (ThreadInterruptedException) {
Console.Write ( "Forcibly " );
}
Console.WriteLine ( "Woken!" );
});
t.Start();
t.Interrupt();
}
}
Forcibly Woken!
|
中斷一個執行緒僅僅釋放它的當前的(或下一個)等待狀態:它並不結束這個執行緒(當然,除非未處理
ThreadInterruptedException異常)。
如果Interrupt被一個未阻止的執行緒呼叫,那麼執行緒將繼續執行直到下一次被阻止時,它丟擲
ThreadInterruptedException異常。用下面的測試避免這個問題:
if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
worker.Interrupt();
這不是一個執行緒安全的方式,因為可能被搶佔了在if語句和worker.Interrupt間。
隨意中斷執行緒是危險的,因為任何框架或第三方方法在呼叫堆疊時可能會意外地在已訂閱的程式碼上收到中斷。這一切將被認為是執行緒被暫時阻止在一個鎖中或同步資 源中,並且所有掛起的中斷將被踢開。如果這個方法沒有被設計成可以被中斷(沒有適當處理finally塊)的物件可能剩下無用的狀態,或資源不完全地被釋 放。
中斷一個執行緒是安全的,當你知道它確切的在哪的時候。稍後我們討論 訊號系統,它提供這樣的一種方式。
3.2 Abort方法
被阻止的執行緒也可以通過Abort方法被強制釋放,這與呼叫Interrupt相似,除了用ThreadAbortException異常代替了
ThreadInterruptedException異常,此外,異常將被重新丟擲在catch裡(在試圖以有好方式處理異常的時候),直到 Thread.ResetAbort在catch中被呼叫;在這期間執行緒的ThreadState為AbortRequested。
在Interrupt 與 Abort 之間最大不同在於它們呼叫一個非阻止執行緒所發生的事情。Interrupt繼續工作直到下一次阻止發生,Abort線上程當前所執行的位置(可能甚至不在 你的程式碼中)丟擲異常。終止一個非阻止的執行緒會帶來嚴重的後果,這在後面的 “終止執行緒”章節中將詳細討論。
4 執行緒狀態
圖1: 執行緒狀態關係圖
你可以通過ThreadState屬性獲取執行緒的執行狀態。圖1將ThreadState列舉為“層”。ThreadState被設計的很恐怖,它以按位計算的方式組合三種狀態“層”,每種狀態層的成員它們間都是互斥的,下面是所有的三種狀態“層”:
- 執行 (running) / 阻止 (blocking) / 終止 (aborting) 狀態(圖1顯示)
- 後臺 (background) / 前臺 (foreground) 狀態 (ThreadState.Background)
- 不建議使用的Suspend 方法(ThreadState.SuspendRequested 和 ThreadState.Suspended)掛起的過程
總的來說,ThreadState是按位組合零或每個狀態層的成員!一個簡單的ThreadState例子:
Unstarted
Running
WaitSleepJoin
Background, Unstarted
SuspendRequested, Background, WaitSleepJoin
(所列舉的成員有兩個從來沒被用過,至少是當前CLR實現上:StopRequested 和 Aborted。)
還有更加複雜的,ThreadState.Running潛在的值為0 ,因此下面的測試不工作:
if ((t.ThreadState & ThreadState.Running) > 0) ...
你必須用按位與非操作符來代替,或者使用執行緒的IsAlive屬性。但是IsAlive可能不是你想要的,它在被阻止或掛起的時候返回true(只有在執行緒未開始或已結束時它才為true)。
假設你避開不推薦使用的Suspend 和 Resume方法,你可以寫一個helper方法除去所有除了第一種狀態層的成員,允許簡單測試計算完成。執行緒的後臺狀態可以通過IsBackground 獨立地獲得,所以實際上只有第一種狀態層擁有有用的資訊。
1 2 3 4 5 6 7 |
public static ThreadState SimpleThreadState (ThreadState ts)
{
return ts & (ThreadState.Aborted | ThreadState.AbortRequested |
ThreadState.Stopped | ThreadState.Unstarted |
ThreadState.WaitSleepJoin);
}
|
ThreadState對除錯或程式概要分析是無價之寶,與之不相稱的是多執行緒的協同工作,因為沒有一個機制存在:通過判斷ThreadState來執行資訊,而不考慮ThreadState期間的變化。
5 等待控制代碼
lock語句(也稱為Monitor.Enter / Monitor.Exit)是執行緒同步結構的一個例子。當lock對一段程式碼或資源實施排他訪問時, 但有些同步任務是相當笨拙的或難以實現的,比如說需要傳輸訊號給等待的工作執行緒使其開始任務執行。
Win32 API擁有豐富的同步系統,這在.NET framework以EventWaitHandle, Mutex 和 Semaphore類展露出來。而一些比有些更有用:例如Mutex類,在EventWaitHandle提供唯一的訊號功能時,大多會成倍提高lock 的效率。
這三個類都依賴於WaitHandle類,儘管從功能上講, 它們相當的不同。但它們做的事情都有一個共同點,那就是,被“點名”,這允許它們繞過作業系統程序工作,而不是隻能在當前程序裡繞過執行緒。
EventWaitHandle有兩個子類:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委託)。這兩個類都派生自它們的基類:它們僅有的不同是它們用不同的引數呼叫基類的建構函式。效能方面,使用Wait Handles系統開銷會花費在微秒間,不會在它們使用的上下文中產生什麼後果。
AutoResetEvent在WaitHandle中是最有用的的類,它連同lock 語句是一個主要的同步結構。
AutoResetEvent
AutoResetEvent就像一個用票通過的旋轉門:插入一張票,讓正確的人通過。類名字裡的“auto”實際上就是旋轉門自動關閉或“重新安排”後 來的人讓其通過。一個執行緒等待或阻止通過在門上呼叫WaitOne方法(直到等到這個“one”,門才開) ,票的插入則由呼叫Set方法。如果由許多執行緒呼叫WaitOne,在門前便形成了佇列,一張票可能來自任意某個執行緒——換言之,任何(非阻止)執行緒要通 過AutoResetEvent物件呼叫Set方法來釋放一個被阻止的的執行緒。
也就是呼叫WaitOne方法的所有執行緒會阻塞到一個等待佇列,其他非阻塞執行緒通過呼叫Set方法來釋放一個阻塞。然後AutoResetEvent繼續阻塞後面的執行緒。
如果Set呼叫時沒有任何執行緒處於等待狀態,那麼控制代碼保持開啟直到某個執行緒呼叫了WaitOne 。這個行為避免了線上程起身去旋轉門和執行緒插入票(哦,插入票是非常短的微秒間的事,真倒黴,你將必須不確定地等下去了!)間的競爭。但是在沒人等的時候 重複地在門上呼叫Set方法不會允許在一隊人都通過,在他們到達的時候:僅有下一個人可以通過,多餘的票都被“浪費了"。
WaitOne 接受一個可選的超時引數——當等待以超時結束時這個方法將返回false,WaitOne在等待整段時間裡也通知離開當前的同步內容,為了避免過多的阻止發生。
Reset作用是關閉旋轉門,也就是無論此時是否已經set過,都將阻塞下一次WaitOne——它應該是開著的。
AutoResetEvent可以通過2種方式建立,第一種是通過建構函式:
1 |
EventWaitHandle wh = new AutoResetEvent ( false );
|
如果布林引數為真,Set方法在構造後立刻被自動的呼叫,也就是說第一個WaitOne會被放行,不會被阻塞,另一個方法是通過它的基類EventWaitHandle:
1 |
EventWaitHandle wh = new EventWaitHandle ( false , EventResetMode.Auto);
|
EventWaitHandle的構造器也允許建立ManualResetEvent(用EventResetMode.Manual定義).
在Wait Handle不在需要時候,你應當呼叫Close方法來釋放作業系統資源。但是,如果一個Wait Handle將被用於程式(就像這一節的大多例子一樣)的生命週期中,你可以發點懶省略這個步驟,它將在程式域銷燬時自動的被銷燬。
接下來這個例子,一個執行緒開始等待直到另一個執行緒發出訊號。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class BasicWaitHandle {
static EventWaitHandle wh = new AutoResetEvent ( false );
static void Main() {
new Thread (Waiter).Start();
Thread.Sleep (1000); // 等一會...
wh.Set(); // OK ——喚醒它
}
static void Waiter() {
Console.WriteLine ( "Waiting..." );
wh.WaitOne(); // 等待通知
Console.WriteLine ( "Notified" );
}
}
Waiting... (pause) Notified.
|
5.1 建立跨程序的EventWaitHandle
EventWaitHandle的構造器允許以“命名”的方式進行建立,它有能力跨多個程序。名稱是個簡單的字串,可能會無意地與別的衝突!如果名字使用了,你將引用相同潛在的EventWaitHandle,除非作業系統建立一個新的,看這個例子:
1 2 |
EventWaitHandle wh = new EventWaitHandle ( false , EventResetMode.Auto,
"MyCompany.MyApp.SomeName" );
|
如果有兩個程式都執行這段程式碼,他們將彼此可以傳送訊號,等待控制代碼可以跨這兩個程序中的所有執行緒。
5.2 任務確認
設想我們希望在後臺完成任務,但又不在每次我們得到任務時再建立一個新的執行緒。我們可以通過一個輪詢的執行緒來完成:等待一個任務,執行它,然後等待下一個 任務。這是一個普遍的多執行緒方案。也就是在建立執行緒上切分內務操作,任務執行被序列化,在多個工作執行緒和過多的資源消耗間排除潛在的不想要的操作。 我們必須決定要做什麼,但是,如果當新的任務來到的時候,工作執行緒已經在忙之前的任務了,設想這種情形下我們需選擇阻止呼叫者直到之前的任務被完成。像這 樣的系統可以用兩個AutoResetEvent物件實現:一個“ready”AutoResetEvent,當準備好的時候,它被工作執行緒呼叫Set方 法;和“go”AutoResetEvent,當有新任務的時候,它被呼叫執行緒呼叫Set方法。在下面的例子中,一個簡單的string欄位被用於決定任 務(使用了volatile 關鍵字宣告,來確保兩個執行緒都可以看到相同版本):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|