lock語句(即Monitor.Enter / Monitor.Exit)多用於當對一段代碼或資源實施排他訪問的線程同步場合, 但在需要傳輸信號給等待的工作線程使其開始任務執行等復雜應用場景下實現同步比較復雜。 .NET framework提供了EventWaitHandle, Mutex 和 Semaphore類用於構建豐富的同步系統,例如MutexEventWaitHandle提供唯一的信號功能時,會成倍提高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種方式創建,第一種是通過構造函數:
EventWaitHandle wh = new AutoResetEvent (false);
如果布爾參數為真,Set方法在構造後立刻被自動的調用,也就是說第一個WaitOne會被放行,不會被阻塞,另一個方法是通過它的基類EventWaitHandle:
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);
EventWaitHandle的構造器也允許創建ManualResetEvent(用EventResetMode.Manual定義).
在Wait Handle不在需要時候,你應當調用Close方法來釋放操作系統資源。但是,如果一個Wait Handle將被用於程序(就像這一節的大多例子一樣)的生命周期中,你可以發點懶省略這個步驟,它將在程序域銷毀時自動的被銷毀。
接下來這個例子,一個線程開始等待直到另一個線程發出信號。
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.
創建跨進程的EventWaitHandle
EventWaitHandle的構造器允許以“命名”的方式進行創建,它有能力跨多個進程。名稱是個簡單的字符串,可能會無意地與別的沖突!如果名字使用了,你將引用相同潛在的EventWaitHandle,除非操作系統創建一個新的,看這個例子:
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto,"MyCompany.MyApp.SomeName");
如果有兩個程序都運行這段代碼,他們將彼此可以發送信號,等待句柄可以跨這兩個進程中的所有線程。
任務確認
設想我們希望在後台完成任務,但又不在每次我們得到任務時再創建一個新的線程。我們可以通過一個輪詢的線程來完成:等待一個任務,執行它,然後等待下一個任務。這是一個普遍的多線程方案。也就是在創建線程上切分內務操作,任務執行被序列化,在多個工作線程和過多的資源消耗間排除潛在的不想要的操作。 我們必須決定要做什麼,但是,如果當新的任務來到的時候,工作線程已經在忙之前的任務了,設想這種情形下我們需選擇阻止調用者直到之前的任務被完成。像這樣的系統可以用兩個AutoResetEvent對象實現:一個“ready”AutoResetEvent,當准備好的時候,它被工作線程調用Set方法;和“go”AutoResetEvent,當有新任務的時候,它被調用線程調用Set方法。在下面的例子中,一個簡單的string字段被用於決定任務(使用了volatile 關鍵字聲明,來確保兩個線程都可以看到相同版本):
class AcknowledgedWaitHandle { static EventWaitHandle ready = new AutoResetEvent (false); static EventWaitHandle go = new AutoResetEvent (false); static volatile string task; static void Main() { new Thread (Work).Start(); // Signal the worker 5 times for (int i = 1; i <= 5; i++) { ready.WaitOne(); // First wait until worker is ready task = "a".PadRight (i, 'h'); // Assign a task go.Set(); // Tell worker to go! } // Tell the worker to end using a null-task ready.WaitOne(); task = null; go.Set(); } static void Work() { while (true) { ready.Set(); // Indicate that we're ready go.WaitOne(); // Wait to be kicked off... if (task == null) return; // Gracefully exit Console.WriteLine (task); } } } ah ahh ahhh ahhhh
注意我們要給task賦null來告訴工作線程退出。在工作線程上調用Interrupt 或Abort 效果是一樣的,倘若我們先調用ready.WaitOne的話。因為在調用ready.WaitOne後我們就知道工作線程的確切位置,不是在就是剛剛在go.WaitOne語句之前,因此避免了中斷任意代碼的復雜性。調用 Interrupt 或 Abort需要我們在工作線程中捕捉異常。
生產者/消費者隊列
另一個普遍的線程方案是在後台工作進程從隊列中分配任務。這叫做生產者/消費者隊列:在工作線程中生產者入列任務,消費者出列任務。這和上個例子很像,除了當工作線程正忙於一個任務時調用者沒有被阻止之外。
生產者/消費者隊列是可縮放的,因為多個消費者可能被創建——每個都服務於相同的隊列,但開啟了一個分離的線程。這是一個很好的方式利用多處理器的系統來限制工作線程的數量一直避免了極大的並發線程的缺陷(過多的內容切換和資源連接)。
在下面例子裡,一個單獨的AutoResetEvent被用於通知工作線程,它只有在用完任務時(隊列為空)等待。一個通用的集合類被用於隊列,必須通過鎖
控制它的訪問以確保線程安全。工作線程在隊列為null任務時結束:
using System; using System.Threading; using System.Collections.Generic; class ProducerConsumerQueue : IDisposable { EventWaitHandle wh = new AutoResetEvent (false); Thread worker; object locker = new object(); Queue<string> tasks = new Queue<string>(); public ProducerConsumerQueue() { worker = new Thread (Work); worker.Start(); } public void EnqueueTask (string task) { lock (locker) tasks.Enqueue (task); wh.Set(); } public void Dispose() { EnqueueTask (null); // Signal the consumer to exit. worker.Join(); // Wait for the consumer's thread to finish. wh.Close(); // Release any OS resources. } void Work() { while (true) { string task = null; lock (locker) if (tasks.Count > 0) { task = tasks.Dequeue(); if (task == null) return; } if (task != null) { Console.WriteLine ("Performing task: " + task); Thread.Sleep (1000); // simulate work... } else wh.WaitOne(); // No more tasks - wait for a signal } } } //Here's a main method to test the queue: class Test { static void Main() { using (ProducerConsumerQueue q = new ProducerConsumerQueue()) { q.EnqueueTask ("Hello"); for (int i = 0; i < 10; i++) q.EnqueueTask ("Say " + i); q.EnqueueTask ("Goodbye!"); } // Exiting the using statement calls q's Dispose method, which // enqueues a null task and waits until the consumer finishes. } }
Performing task: Hello
Performing task: Say 1
Performing task: Say 2
Performing task: Say 3
...
...
Performing task: Say 9
Goodbye!
注意我們明確的關閉了Wait Handle在ProducerConsumerQueue被銷毀的時候,因為在程序的生命周期中我們可能潛在地創建和銷毀許多這個類的實例。
ManualResetEvent
ManualResetEvent是AutoResetEvent變化的一種形式,它的不同之處在於:在線程被WaitOne的調用而通過的時候,它不會自動地reset,這個過程就像大門一樣——調用Set打開門,允許任何數量的已執行WaitOne的線程通過;調用Reset關閉大門,可能會引起一系列的“等待者”直到下次門打開。
你可以用一個布爾字段"gateOpen" (用 volatile 關鍵字來聲明)與"spin-sleeping" – 方式結合——重復地檢查標志,然後讓線程休眠一段時間的方式,來模擬這個過程。
ManualResetEvent有時被用於給一個完成的操作發送信號,又或者一個已初始化正准備執行工作的線程。
互斥(Mutex)
Mutex提供了與C#的lock語句同樣的功能,這使它大多時候變得的冗余了。它的優勢在於它可以跨進程工作——提供了一計算機范圍的鎖而勝於程序范圍的鎖。
Mutex是相當快的,而lock 又要比它快上數百倍,獲取Mutex需要花費幾微秒,獲取lock需花費數十納秒(假定沒有阻止)。
對於一個Mutex類,WaitOne獲取互斥鎖,當被搶占後時發生阻止。互斥鎖在執行了ReleaseMutex之後被釋放,就像C#的lock語句一樣,Mutex只
能從獲取互斥鎖的這個線程上被釋放。
Mutex在跨進程的普遍用處是確保在同一時刻只有一個程序的的實例在運行,下面演示如何使用:
class OneAtATimePlease { // Use a name unique to the application (eg include your company URL) static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo"); static void Main() { // Wait 5 seconds if contended – in case another instance // of the program is in the process of shutting down. if (!mutex.WaitOne (TimeSpan.FromSeconds (5), false)) { Console.WriteLine ("Another instance of the app is running. Bye!"); return; } try { Console.WriteLine ("Running - press Enter to exit"); Console.ReadLine(); } finally { mutex.ReleaseMutex(); } } }
Mutex有個好的特性是,如果程序結束時而互斥鎖沒通過ReleaseMutex首先被釋放,CLR將自動地釋放Mutex。
Semaphore
Semaphore就像一個夜總會:它有固定的容量,這由保镖來保證,一旦它滿了就沒有任何人可以再進入這個夜總會,並且在其外會形成一個隊列。然後,當人一個人離開時,隊列頭的人便可以進入了。構造器需要至少兩個參數——夜總會的活動的空間,和夜總會的容量。
Semaphore 的特性與Mutex 和 lock有點類似,除了Semaphore沒有“所有者”——它是不可知線程的,任何在Semaphore內的線程都可以調用Release,而Mutex 和 lock僅有那些獲取了資源的線程才可以釋放它。
在下面的例子中,10個線程執行一個循環,在中間使用Sleep語句。Semaphore確保每次只有不超過3個線程可以執行Sleep語句:
class SemaphoreTest { static Semaphore s = new Semaphore (3, 3); // Available=3; Capacity=3 static void Main() { for (int i = 0; i < 10; i++) new Thread (Go).Start(); } static void Go() { while (true) { s.WaitOne(); Thread.Sleep (100); // Only 3 threads can get here at once s.Release(); } } }
WaitAny, WaitAll 和 SignalAndWait
除了Set 和 WaitOne方法外,在類WaitHandle中還有一些用來創建復雜的同步過程的靜態方法。
WaitAny, WaitAll 和 SignalAndWait使跨多個可能為不同類型的等待句柄變得容易。
SignalAndWait可能是最有用的了:他在某個WaitHandle上調用WaitOne,並在另一個WaitHandle上自動地調用Set。你可以在一對EventWaitHandle上裝配兩個線程,而讓它們在某個時間點“相遇”,這馬馬虎虎地合乎規范。AutoResetEvent 或 ManualResetEvent都無法使用這個技巧。第一個線程像這樣:
WaitHandle.SignalAndWait (wh1, wh2);
同時第二個線程做相反的事情:
WaitHandle.SignalAndWait (wh2, wh1);
WaitHandle.WaitAny等待一組等待句柄任意一個發出信號,WaitHandle.WaitAll等待所有給定的句柄發出信號。與票據旋轉門的例子類似,這些方法可能同時地等待所有的旋轉門——通過在第一個打開的時候(WaitAny情況下),或者等待直到它們所有的都打開(WaitAll情況下)。
WaitAll 實際上是不確定的值,因為這與單元模式線程——從COM體系遺留下來的問題,有著奇怪的聯系。WaitAll 要求調用者是一個多線程單元——剛巧是單元模式最適合——尤其是在 Windows Forms程序中,需要執行任務像與剪切板結合一樣庸俗!
幸運地是,在等待句柄難使用或不適合的時候,.NET framework提供了更先進的信號結構——Monitor.Wait 和 Monitor.Pulse。