C#多线程之线程同步篇2
在上一篇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。