建议72:在线程同步中使用信号量

所谓线程同步,就是多个线程在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定。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 += "我开始等待别的线程给我信号,才愿意继续下去" + Environment.NewLine;
  11. autoResetEvent.WaitOne();
  12. label1.Text += "我继续做一些工作,然后结束了!";
  13. //省略工作代码
  14. });
  15. tWork.IsBackground = true;
  16. tWork.Start();
  17. }
  18.  
  19. private void buttonSet_Click(object sender, EventArgs e)
  20. {
  21. //给在autoResetEvent上等待的线程一个信号
  22. autoResetEvent.Set();
  23. }

这是一个简单的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 += "我开始等待别的线程给我信号,才愿意继续下去" + Environment.NewLine;
  17. autoResetEvent.WaitOne();
  18. label1.Text += "我继续做一些工作,然后结束了!";
  19. //省略工作代码
  20. });
  21. tWork1.IsBackground = true;
  22. tWork1.Start();
  23. }
  24.  
  25. private void StartThread2()
  26. {
  27. Thread tWork2 = new Thread(() =>
  28. {
  29. label2.Text = "线程2启动..." + Environment.NewLine;
  30. label2.Text += "开始处理一些实际的工作" + Environment.NewLine;
  31. //省略工作代码
  32. label2.Text += "我开始等待别的线程给我信号,才愿意继续下去" + Environment.NewLine;
  33. autoResetEvent.WaitOne();
  34. label2.Text += "我继续做一些工作,然后结束了!";
  35. //省略工作代码
  36. });
  37. tWork2.IsBackground = true;
  38. tWork2.Start();
  39. }
  40.  
  41. private void buttonSet_Click(object sender, EventArgs e)
  42. {
  43. //给在autoResetEvent上等待的线程一个信号
  44. autoResetEvent.Set();
  45. }

这个例子的本意是要让新起的两个工作线程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();
  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();
  32. }

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

编写高质量代码改善C#程序的157个建议——建议72:在线程同步中使用信号量的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  2. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  3. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  4. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  5. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  6. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  7. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  8. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

  10. 编写高质量代码改善C#程序的157个建议——建议150:使用匿名方法、Lambda表达式代替方法

    建议150:使用匿名方法.Lambda表达式代替方法 方法体如果过小(如小于3行),专门为此定义一个方法就会显得过于繁琐.比如: static void SampeMethod() { List< ...

随机推荐

  1. java之IO整理(上)

    /*//创建一个新文件 public static void main(String[] args) { File file=new File("D:\\hello.txt"); ...

  2. WordSmith2013-6-19

    WordSmith Good Evening Ladies and Gentlemen,I’am Jason,I’m pleasured  to be wordsmith tonight. First ...

  3. SharePoint 事件 7363:对象缓存:缓存使用的超级读者帐户没有足够的权限访问SharePoint数据库。

    转自MSND:http://technet.microsoft.com/zh-cn/library/ff758656(v=office.14) 对象缓存存储 Microsoft SharePoint ...

  4. VRF实例说明

    Virtual Routing Forwarding       VPN路由转发表,也称VPN-instance(VPN实例),是PE为直接相连的site建立并维护的一个专门实体,每个site在PE上 ...

  5. Java8函数式接口和Lambda表达式

    两者关系: Lambda表达式就是函数式接口(FunctionalInterface)实现的快捷方式,它相当于函数式接口实现的实例,因为在方法中可以使用Object作为参数,所以把Lambda表达式作 ...

  6. Eclipse注释配置

    新的文件/** * @ClassName: ${type_name}  * @Description: ${todo} * @author ${user} * @date ${date} ${time ...

  7. jbpm角色审批

    可分配是一个部门或角色组,也可以选择一个表达式操作,提交任务时可以根据权限过滤这个部门或组的用户中选择一个可操作用户 <task name="审核">          ...

  8. sha1sum校验下载的文件

    [root@mhc1 test]# sha1sum Percona-XtraBackup-2.4.8-r97330f7-jessie-x86_64-bundle.tara9c6b1c7cb3bf98b ...

  9. Python与Go插入排序

    #!/usr/bin/env python # -*- coding: utf-8 -*- # 插入排序 # 时间复杂度 O(n^2) import time def logger(func): st ...

  10. 读书笔记 Week6 2018-4-12

    Chap 24 重构 读书笔记 一.需求的变更 单纯就科目学习中的小项目来说,目标在一开始便被明确下来,即可定义一份严谨的列表来描述功能.故在原来的编程经历中,只要上交了程序便一切都没事儿了,也没有重 ...