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

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

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

图 同步功能类类图

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

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

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

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


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

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

  1.  
  2. AutoResetEvent autoResetEvent = new AutoResetEvent(false);
  3.  
  4. private void buttonStartAThread_Click(object sender, EventArgs e)
  5. {
  6. Thread tWork = new Thread(() =>
  7. {
  8. label1.Text = "线程启动..." + Environment.NewLine;
  9. label1.Text += "开始处理一些实际的工作" + Environment.NewLine;
  10. //省略工作代码
  11. label1.Text += "我开始等待别的线程给我信号,才愿意继续下去" + 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();
  24. }

这是一个简单的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接收到这个信号,开始继续工作。

这个例子相当简单,但是已经完整说明了信号机制的工作原理。

2:AutoResetEvent和ManualResetEvent的区别

AutoResetEvent和ManualResetEvent有这样的区别:前者在发送信号完毕后(即调用Set方法),自动将自己的阻滞状态设置为false,而后者需要进行手动设定。可以通过一个例子来说明这种区别:

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

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

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

3:应用实例

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

  1.  
  2. AutoResetEvent autoResetEvent = new AutoResetEvent(false);
  3.  
  4. private void buttonStartAThread_Click(object sender, EventArgs e)
  5. {
  6. Thread tClient = new Thread(() =>
  7. {
  8. while (true)
  9. {
  10. //等3秒,3秒没有信号,显示断开
  11. //有信号,则显示更新
  12. bool re = autoResetEvent.WaitOne(3000);
  13. if (re)
  14. {
  15. label1.Text = string.Format("时间:{0},{1}", DateTime.Now.ToString(), "保持连接状态");
  16. }
  17. else
  18. {
  19. label1.Text = string.Format("时间:{0},{1}", DateTime.Now.ToString(), "断开,需要重启");
  20. }
  21. }
  22. });
  23. tClient.IsBackground = true;
  24. tClient.Start();
  25. }
  26.  
  27. private void buttonSet_Click(object sender, EventArgs e)
  28. {
  29. //模拟发送心跳数据
  30. autoResetEvent.Set();
  31. }


备注:由本问题带来一个Winform跨线程控件赋值和操作的问题。由于在本示例中不影响上面代码的运行,所以没有涉及,但是回复中有人提出来,所以提前简述一下Winform的线程模型:

在Winform框架中,有一个ISynchronizeInvoke接口,所有的UI元素(表现为Control)都继承了该接口。其中,接口中的InvokdRequired属性表示了当前线程是否是创建它的线程。接口中的Invoke和BeginInvoke方法负责将消息发送到消息队列中,这样,UI线程就能够正确处理它。

具体到代码中,对于夸线程控件赋值,可以采用下面的方法:

  1. this.label1.BeginInvoke(new Action(()=>
  2. {
  3. this.label1.Text = "跨线程中赋值";
  4. }));

[No000017D]改善C#程序的建议6:在线程同步中使用信号量的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议——建议72:在线程同步中使用信号量

    建议72:在线程同步中使用信号量 所谓线程同步,就是多个线程在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定.C#中对象的类型分为引用类型和值类型.CLR在这两种类型上的等待是不一样 ...

  2. 改善C#程序的建议6:在线程同步中使用信号量

    原文:改善C#程序的建议6:在线程同步中使用信号量 所谓线程同步,就是多个线程之间在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定.C#中对象的类型分为引用类型和值类型.CLR在这两 ...

  3. 改善C#程序的建议3:在C#中选择正确的集合进行编码

    要选择正确的集合,我们首先要了解一些数据结构的知识.所谓数据结构,就是相互之间存在一种或多种特定关系的数据元素的集合.结合下图,我们看一下对集合的分类. 集合分类 在上图中,可以看到,集合总体上分为线 ...

  4. [No000017A]改善C#程序的建议3:在C#中选择正确的集合进行编码

    要选择正确的集合,我们首先要了解一些数据结构的知识.所谓数据结构,就是相互之间存在一种或多种特定关系的数据元素的集合.结合下图,我们看一下对集合的分类. 集合分类 在上图中,可以看到,集合总体上分为线 ...

  5. 改善C#程序的建议10:用Parallel简化Task

    在命名空间System.Threading.Tasks下,有一个静态类Parallel简化了在同步状态下的Task的操作.Parallel主要提供了3个有用的方法:For.ForEach.Invoke ...

  6. 改善C#程序的建议8:避免锁定不恰当的同步对象

    原文:改善C#程序的建议8:避免锁定不恰当的同步对象 在C#中让线程同步的另一种编码方式就是使用线程锁.所谓线程锁,就是锁住一个资源,使得应用程序只能在此刻有一个线程访问该资源.可以用下面这句不是那么 ...

  7. 改善C#程序的建议7:正确停止线程

    原文:改善C#程序的建议7:正确停止线程 开发者总尝试对自己的代码有更多的控制.“让那个还在工作的线程马上停止下来”就是诸多要求中的一种.然而事与愿违,这里面至少存在两个问题: 第一个问题是:正如线程 ...

  8. [转]改善C#程序的建议4:C#中标准Dispose模式的实现

    需要明确一下C#程序(或者说.NET)中的资源.简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类: 托管资源:由CLR管理分配和释放的资源,即由CLR里new出来的对象: 非托管资源:不 ...

  9. 改善C#程序的建议1:非用ICloneable不可的理由

    原文:改善C#程序的建议1:非用ICloneable不可的理由 好吧,我承认,这是一个反标题,实际的情况是:我找不到一个非用ICloneable不可的理由.事实上,接口ICloneable还会带来误解 ...

随机推荐

  1. iOS 在tableview的侧滑事件里执行tableView.selectRow无效的解决办法

    很奇怪的问题,在执行默认选中一个cell的时候,突然发现这句话不起作用了 (我的场景是:当前cell侧滑删除后,默认选中上一个cell) 搞了半天,终于发现罪魁祸首竟然是因为:这句话写在了侧滑事件的方 ...

  2. 茗洋Easy UI 1.3.5 部分问题解决系列专题[自定义alert关闭时间,自动关]

    [评论,楼层数为30的倍数的,我送你我自己的博客园的皮肤,该博客参与活动] 这次我又给大家带来的EasyUI的我研究拓展的新特性 我使用的是  EasyUI 1.3.5版本的,项目是ASP.NET M ...

  3. golang ----gc问题

    go程序内存占用大的问题 这个问题在我们对后台服务进行压力测试时发现,我们模拟大量的用户请求访问后台服务,这时各服务模块能观察到明显的内存占用上升.但是当停止压测时,内存占用并未发生明显的下降.花了很 ...

  4. 解决ScrollView嵌套RecyclerView的显示及滑动问题

        项目中时常需要实现在ScrollView中嵌入一个或多个RecyclerView.这一做法通常会导致如下几个问题 页面滑动卡顿 ScrollView高度显示不正常 RecyclerView内容 ...

  5. MyBean通用报表插件介绍

    特性: 1.基于MyBean插件平台.可以在任何插件中无缝调用显示. 2.其他窗体中无需引用报表控件.就可以拥有报表的设计预览打印等功能. 3.甚至可以不用带包,制作报表插件,也就是说你可以将RM的报 ...

  6. vue cli 项目的提交

    前提: 配置git.以及git的ssh key信息 假设已经都安装好了,此处我用vue项目为例,因为vue-cli已经默认为我生成了ignore文件 在项目目录 初始化本地仓库,会创建一个.git目录 ...

  7. GNU make使用(二)

    [时间:2017-06] [状态:Open] [关键词:makefile,gcc,编译,shell命令,目标文件] 0 引言及目标 之前使用Makefile都是把源文件和目标文件放到同一个目录编译.近 ...

  8. Java知多少(48)try语句的嵌套

    Try语句可以被嵌套.也就是说,一个try语句可以在另一个try块内部.每次进入try语句,异常的前后关系都会被推入堆栈.如果一个内部的try语句不含特殊异常的catch处理程序,堆栈将弹出,下一个t ...

  9. python安装模块

    pychram安装模块,非常简单!

  10. linux swoole

    swoole安装需要:linux7 +php5.3.10以上版本+gcc-4.4 或更高版本 下载地址: https://github.com/swoole/swoole-src/releases h ...