在上一篇C#多線程之線程同步1中,我們主要學習了執行基本的原子操作、使用Mutex構造以及SemaphoreSlim構造,在這一篇中我們主要學習如何使用AutoResetEvent構造、ManualResetEventSlim構造和CountDownEvent構造。
四、使用AutoResetEvent構造
在這一小節中,我們將學習如何使用AutoResetEvent構造從一個線程向另一個線程發送通知。AutoResetEvent通知一個等待線程某個事件已經發生。具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe04 7 { 8 class Program 9 { 10 private static AutoResetEvent workerEvent = new AutoResetEvent(false); 11 private static AutoResetEvent mainEvent = new AutoResetEvent(false); 12 13 static void Process(int seconds) 14 { 15 WriteLine("Starting a long running work..."); 16 Sleep(TimeSpan.FromSeconds(seconds)); 17 WriteLine("Work is done!"); 18 workerEvent.Set(); 19 WriteLine("Waiting for a main thread to complete its work"); 20 mainEvent.WaitOne(); 21 WriteLine("Starting second operation..."); 22 Sleep(TimeSpan.FromSeconds(seconds)); 23 WriteLine("Work is done!"); 24 workerEvent.Set(); 25 } 26 27 static void Main(string[] args) 28 { 29 var t = new Thread(() => Process(10)); 30 t.Start(); 31 32 WriteLine("Waiting for another thread to complete work"); 33 workerEvent.WaitOne(); 34 WriteLine("First operation is completed!"); 35 WriteLine("Performing an operation on a main thread"); 36 Sleep(TimeSpan.FromSeconds(5)); 37 mainEvent.Set(); 38 WriteLine("Now running the second operation on a second thread"); 39 workerEvent.WaitOne(); 40 WriteLine("Second operation is completed!"); 41 } 42 } 43 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在第10~11行代碼處,我們定義了兩個AutoResetEvent實例:workerEvent和mainEvent。workerEvent用於從新建線程中向主線程發送通知,mainEvent用於從主線程向新建線程發送通知。在調用AutoResetEvent的構造方法的時候,我們給該構造方法的“initialState”參數傳遞了false值,指定AutoResetEvent實例的初始狀態為“無信號狀態”,這意味著調用AutoResetEvent實例的“WaitOne”方法的線程將會被阻塞,直到我們調用AutoResetEvent實例的“Set”方法之後,該線程才會繼續執行。如果我們將AutoResetEvent類的構造方法的“initialState”參數值設置為true,則AutoResetEvent實例的初始狀態為“信號狀態”,那麼第一個調用AutoResetEvent實例的“WaitOne”方法的線程將會被立即執行,然後AutoResetEvent實例的狀態自動變為“無信號狀態”,這個時候,當我們再次調用AutoResetEvent的“WaitOne”方法後,必須在另一個線程中調用AutoResetEvent的“Set”方法才能繼續執行當前的線程。
在第29行代碼處,我們創建了一個新的線程用於執行“Process”方法,並在第30行代碼處啟動線程。
在第33行代碼處,我們調用AutoResetEvent實例workerEvent的“WaitOne”方法,導致主線程被阻塞,然而在我們在第29行代碼處創建的線程中,我們調用了AutoResetEvent實例WorkerEvent的“Set”方法,因此,主線程得以繼續執行。當執行到第20行代碼處,我們在新建線程中調用了AutoResetEvent實例mainEvent的“WaitOne”方法,因此導致新建線程被阻塞,然而在主線程執行到第37行代碼處,我們調用了AutoResetEvent實例mainEvent的“Set”方法,因此,新建線程得以繼續執行。而主線程在執行到第39行代碼處,主線程又被阻塞,而新建線程執行到第24行代碼處,導致主線程得以繼續執行,因此,主線程執行到第40行代碼,控制台應用程序正常結束。
AutoResetEvent是kernel-time構造,因此,如果沒有必要,我們建議使用下一節介紹的ManualResetEventslim來替代AutoResetEvent。
五、使用ManualResetEventSlim構造
在這一小節中,我們將學習如何使用ManualResetEventSlim構造在多個線程之間更加靈活地發送通知。具體步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe05 7 { 8 class Program 9 { 10 private static ManualResetEventSlim mainEvent = new ManualResetEventSlim(false); 11 12 static void TravelThroughGates(string threadName, int seconds) 13 { 14 WriteLine($"{threadName} falls to sleep"); 15 Sleep(TimeSpan.FromSeconds(seconds)); 16 WriteLine($"{threadName} waits for the gates to open!"); 17 mainEvent.Wait(); 18 WriteLine($"{threadName} enters the gates!"); 19 } 20 21 static void Main(string[] args) 22 { 23 var t1 = new Thread(() => TravelThroughGates("Thread 1", 5)); 24 var t2 = new Thread(() => TravelThroughGates("Thread 2", 6)); 25 var t3 = new Thread(() => TravelThroughGates("Thread 3", 12)); 26 27 t1.Start(); 28 t2.Start(); 29 t3.Start(); 30 31 Sleep(TimeSpan.FromSeconds(6)); 32 WriteLine("The gates are now open!"); 33 mainEvent.Set(); 34 Sleep(TimeSpan.FromSeconds(2)); 35 mainEvent.Reset(); 36 WriteLine("The gates have been closed!"); 37 Sleep(TimeSpan.FromSeconds(10)); 38 WriteLine("The gates are now open for the second time!"); 39 mainEvent.Set(); 40 Sleep(TimeSpan.FromSeconds(2)); 41 WriteLine("The gates have been closed!"); 42 mainEvent.Reset(); 43 } 44 } 45 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:
在第10行代碼處,我們定義了一個ManualResetEventSlim類型的實例mainEvent,並給它的構造方法的“initialState”參數傳遞了false值,表示該對象的初始狀態為“無信號狀態”。
在第23~25行代碼處,我們創建了三個線程t1、t2和t3。這三個線程都用於執行“TraveThroughGates”方法,在該方法的內部,我們調用了ManualResetEventSlim實例mainEvent的“Wait”方法,以阻塞t1、t2和t3線程的執行。
在第31行代碼處,我們讓主線程阻塞6秒鐘,在這六秒鐘內,線程t1和t2都執行到第17行代碼處,這個時候線程t1和t2都阻塞,並且等待mainEvent的“Set”方法被調用,以接收信號後繼續執行。主線程阻塞6秒鐘後,會執行第33行代碼,執行完畢這行代碼之後,線程t1和t2都會接收到通知,因此,線程t1和t2都會繼續往下執行,從而都執行第18行代碼,之後線程t1和t2執行完畢,結束。
由於線程t3在主線程執行到第33行代碼處的時候,還在阻塞(因為執行了第15行代碼)中,因此線程t3在主線程執行到第33行代碼處的時候不受影響,繼續阻塞。
當主線程執行到第34行代碼處的時候,線程t3依然在阻塞狀態中。在主線程執行了第35行代碼之後,mainEvent被重置為“無信號狀態”。當主線程執行到第37行代碼處,主線程被阻塞10秒鐘。在主線程被阻塞的10秒鐘內,線程t3會執行到第17行代碼處,從而t3線程被阻塞,等待通知的到來,才能繼續執行。
當主線程阻塞10秒鐘之後,會執行第39行代碼,從而導致線程t3繼續執行,因此會執行第18行代碼,線程t3結束。
然後主線程阻塞2秒鐘後,又將mainEvent重置為“無信號狀態”,然後主線程結束。
六、使用CountdownEvent構造
在這一小節中,我們將學習如何使用CountdownEvent構造等待發送一定數量的通知後,才繼續執行被阻塞的線程。學習步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System; 2 using System.Threading; 3 using static System.Console; 4 using static System.Threading.Thread; 5 6 namespace Recipe06 7 { 8 class Program 9 { 10 private static CountdownEvent countdown = new CountdownEvent(2); 11 12 static void PerformOperation(string message, int seconds) 13 { 14 Sleep(TimeSpan.FromSeconds(seconds)); 15 WriteLine(message); 16 countdown.Signal(); 17 } 18 19 static void Main(string[] args) 20 { 21 WriteLine("Starting two operations"); 22 var t1 = new Thread(() => PerformOperation("Operation 1 is completed", 4)); 23 var t2 = new Thread(() => PerformOperation("Operation 2 is completed", 8)); 24 25 t1.Start(); 26 t2.Start(); 27 countdown.Wait(); 28 WriteLine("Both operations have been completed."); 29 countdown.Dispose(); 30 } 31 } 32 }
3、運行該控制台應用程序,運行效果如下圖所示:
在第10行代碼處,我們創建了一個CountdownEvent的實例countdown,並給該構造方法的“initialCount”參數傳遞了數值2,表示我們希望等待2個通知發送完畢後,被阻塞的線程才能繼續執行。
在第22~23行代碼處,我們創建了兩個新線程用於執行“PerformOperation”方法,在該方法中,我們調用了countdown的“Signal”方法,用於發送通知,並減小CountdownEvent的CurrentCount的值,當CurrentCount的值減少到0時,被阻塞的線程才能繼續執行。
在第27行代碼處,我們在主線程中調用了countdown的“Wait”方法,從而主線程被阻塞,直到接收到通知並且CurrentCount的值為0時,主線程才能繼續執行。
注意,如果將第10行代碼處的2修改為3,再次運行該程序,主線程會一直等待,不會結束,因為CurrentCount的值沒有減少到0。