• 【转载】C# 在线程同步中使用信号量


    转载:http://book.51cto.com/art/201109/292346.htm

    所谓线程同步,就是多个线程在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定。C#中对象的类型分为引用类型和值类型。CLR在这两种类型上的等待是不一样的。我们可以简单地理解为在CLR中,值类型是不能被锁定的,即不能在一个值类型对象上执行等待。而在引用类型上的等待机制,又分为两类:锁定和信号同步。

    锁定使用关键字lock和类型Monitor。两者没有实质区别,前者其实是后者的语法糖。这是最常用的同步技术。

    本建议主要讨论信号同步。信号同步机制中涉及的类型都继承自抽象类WaitHandle,这些类型有EventWaitHandle(类型化为AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex。见类图6-3。

     
    图6-3 同步功能类的类图

    EventWaitHandle(子类为AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex都继承自WaitHandle,所以它们底层的原理是一致的,维护的都是一个系统内核句柄。不过我们仍需简单地区分这三个类的类型。

    EventWaitHandle维护一个由内核产生的布尔类型对象(称为“阻滞状态”),如果其值为false,那么在它上面等待的线程就阻塞。可以调用类型的Set方法将其值设置为true,解除阻塞。EventWaitHandle类型有两个子类AutoResetEvent和ManualResetEvent,它们的区别并不大,本建议接下来会针对它们阐述如何正确使用信号量。

    Semaphore维护一个由内核产生的整型变量,如果其值为0,则在它上面等待的线程就会阻塞;如果其值大于0,则解除阻塞,同时,每解除一个线程阻塞,其值就减1。

    EventWaitHandle和Semaphore提供的都是单应用程序域内的线程同步功能,Mutex则不同,它为我们提供了跨应用程序域阻塞和解除阻塞线程的能力。

    使用信号机制提供线程同步的一个简单例子如下所示:

    1. AutoResetEvent autoResetEvent = new AutoResetEvent(false);  
    2.  
    3. private void buttonStartAThread_Click(object sender, EventArgs e)  
    4. {  
    5.     Thread tWork = new Thread(() => 
    6.     {  
    7.         label1.Text = "线程启动..." + Environment.NewLine;  
    8.         label1.Text += "开始处理一些实际的工作" + Environment.NewLine;  
    9.         //省略工作代码  
    10.         label1.Text += "我开始等待别的线程给我信号,才愿意继续下去" +   
    11.             Environment.NewLine;   
    12.         autoResetEvent.WaitOne();  
    13.         label1.Text += "我继续做一些工作,然后结束了!";  
    14.         //省略工作代码  
    15.     });  
    16.     tWork.IsBackground = true;  
    17.     tWork.Start();  
    18. }  
    19.  
    20. private void buttonSet_Click(object sender, EventArgs e)  
    21. {  
    22.     //给在autoResetEvent上等待的线程一个信号  
    23.     autoResetEvent.Set();  

    这是一个简单的Winform窗体程序,其中一个按钮负责开启一个新的线程,另一个按钮负责给刚开启的那个线程发送信号。现在详细解释其中发生的事情。

    1. AutoResetEvent autoResetEvent = new AutoResetEvent(false); 

    这段代码创建了一个同步类型对象autoResetEvent,它设置自己的默认阻滞状态是false。这意味着任何在它上面进行等待的线程都将被阻滞。所谓等待,就是在线程中应用:

    1. autoResetEvent.WaitOne(); 

    这说明tWork开始在autoResetEvent上等待任何其他地方给它的信号。信号来了,则tWork开始继续工作,否则就一直等着(即阻滞)。接下来看看主线程中的这句代码(本例中即UI线程,它相对于线程tWork来说,就是一个“另外的线程”):

    1. autoResetEvent.Set(); 

    主线程通过上面这句代码向在autoResetEvent上等待的线程tWork上下文发送信号,即将tWork的阻滞状态设置为true。tWork接收到这个信号后,开始继续工作。 这个例子相当简单,但是已经完整说明了信号机制的工作原理。 AutoResetEvent和ManualResetEvent的区别是:前者在发送信号完毕后(即调用Set方法),会自动将自己的阻滞状态设置为false,而后者则需要进行手动设定。通过一个例子来说明这种区别,如下所示:

    1. AutoResetEvent autoResetEvent = new AutoResetEvent(false);  
    2.  
    3. private void buttonStartAThread_Click(object sender, EventArgs e)  
    4. {  
    5.     StartThread1();  
    6.     StartThread2();  
    7. }  
    8.  
    9. private void StartThread1()  
    10. {  
    11.     Thread tWork1 = new Thread(() => 
    12.     {  
    13.         label1.Text = "线程1启动..." + Environment.NewLine;  
    14.         label1.Text += "开始处理一些实际的工作" + Environment.NewLine;  
    15.         //省略工作代码  
    16.         label1.Text += "我开始等待别的线程给我信号,才愿意继续下去" +   
    17.             Environment.NewLine;  
    18.         autoResetEvent.WaitOne();  
    19.         label1.Text += "我继续做一些工作,然后结束了!";  
    20.         //省略工作代码  
    21.     });  
    22.     tWork1.IsBackground = true;  
    23.     tWork1.Start();  
    24. }  
    25.  
    26. private void StartThread2()  
    27. {  
    28.     Thread tWork2 = new Thread(() => 
    29.     {  
    30.         label2.Text = "线程2启动..." + Environment.NewLine;  
    31.         label2.Text += "开始处理一些实际的工作" + Environment.NewLine;  
    32.         //省略工作代码  
    33.         label2.Text += "我开始等待别的线程给我信号,才愿意继续下去" +   
    34.              Environment.NewLine;  
    35.         autoResetEvent.WaitOne();  
    36.         label2.Text += "我继续做一些工作,然后结束了!";  
    37.         //省略工作代码  
    38.     });  
    39.     tWork2.IsBackground = true;  
    40.     tWork2.Start();  
    41. }  
    42.  
    43. private void buttonSet_Click(object sender, EventArgs e)  
    44. {  
    45.     //给在autoResetEvent上等待的线程一个信号  
    46.     autoResetEvent.Set();  

    这个例子的本意是要让新起的两个工作线程tWork1和tWork2都阻滞,直到收到主线程的信号再继续工作。而程序运行的结果是,只有一个工作线程继续工作,另外一个工作线程则继续保持阻滞状态。我想可能大家都已经猜到原因了,即AutoResetEvent发送信号完毕就在内核中自动将自己的状态设置回false了,所以另外一个工作线程相当于根本没有收到主线程的信号。

    要修正这个问题,可以使用ManualResetEvent。大家可以将其换成ManualResetEvent试一下。

    最后,再举一个需要用到线程同步的实际例子:模拟网络通信。客户端在运行过程中,服务器每隔一段的时间会给客户端发送心跳数据。实际工作中的服务器和客户端在网络中是两台不同的终端,不过在这个例子中我们将其进行了简化:工作线程tClient模拟客户端,主线程(UI线程)模拟服务器端。客户端每3秒检测是否收到服务器的心跳数据,如果没有心跳数据,则显示网络连接断开。代码如下所示:

    1. AutoResetEvent autoResetEvent = new AutoResetEvent(false);  
    2.  
    3. private void buttonStartAThread_Click(object sender, EventArgs e)  
    4. {  
    5.     Thread tClient = new Thread(() => 
    6.         {  
    7.             while (true)  
    8.             {  
    9.                 //等3秒,3秒没有信号,显示断开  
    10.                 //有信号,则显示更新  
    11.                 bool re = autoResetEvent.WaitOne(3000);  
    12.                 if (re)  
    13.                 {  
    14.                     label1.Text = string.Format("时间:{0},{1}",   
    15.                         DateTime.Now.ToString(), "保持连接状态");  
    16.                 }  
    17.                 else  
    18.                 {  
    19.                     label1.Text = string.Format("时间:{0},{1}",   
    20.                         DateTime.Now.ToString(), "断开,需要重启");  
    21.                 }  
    22.             }  
    23.         });  
    24.     tClient.IsBackground = true;  
    25.     tClient.Start();  
    26. }  
    27.  
    28. private void buttonSet_Click(object sender, EventArgs e)  
    29. {  
    30.     //模拟发送心跳数据  
    31.     autoResetEvent.Set();  
  • 相关阅读:
    linux系统root用户忘记密码的重置方法
    Linux系统的初始化配置
    LINUX awk 函数
    随机产生一个密码,要求同时包含大小写以及数字这三种字符。
    sed 函数 linux
    grep 函数
    linux sort 函数
    从零开始的JAVA -4. 运算符与表达式
    cp
    PATH
  • 原文地址:https://www.cnblogs.com/zhxhdean/p/2547760.html
Copyright © 2020-2023  润新知