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

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

所谓线程同步,就是多个线程之间在某个对象上执行等待(也可理解为锁定该对象),直到该对象被解除锁定。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:使用信号机制提供线程同步的一个简单的例子

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


AutoResetEvent autoResetEvent = new AutoResetEvent(false);

private void buttonStartAThread_Click(object sender, EventArgs e)
{
Thread tWork = new Thread(() =>
{
label1.Text = "线程启动..." + Environment.NewLine;
label1.Text += "开始处理一些实际的工作" + Environment.NewLine;
//省略工作代码
label1.Text += "我开始等待别的线程给我信号,才愿意继续下去" + Environment.NewLine;
autoResetEvent.WaitOne();
label1.Text += "我继续做一些工作,然后结束了!";
//省略工作代码
});
tWork.IsBackground = true;
tWork.Start();
}

private void buttonSet_Click(object sender, EventArgs e)
{
//给在autoResetEvent上等待的线程一个信号
autoResetEvent.Set();
}

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


AutoResetEvent autoResetEvent = new AutoResetEvent(false);

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


autoResetEvent.WaitOne();

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


autoResetEvent.Set();

主线程通过上面这句代码负责向在autoResetEvent上等待的线程tWork上下文发送信号,即将tWork的阻滞状态设置为true。tWork接收到这个信号,开始继续工作。

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

2:AutoResetEvent和ManualResetEvent的区别

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


AutoResetEvent autoResetEvent = new AutoResetEvent(false);

private void buttonStartAThread_Click(object sender, EventArgs e)
{
StartThread1();
StartThread2();
}

private void StartThread1()
{
Thread tWork1 = new Thread(() =>
{
label1.Text = "线程1启动..." + Environment.NewLine;
label1.Text += "开始处理一些实际的工作" + Environment.NewLine;
//省略工作代码
label1.Text += "我开始等待别的线程给我信号,才愿意继续下去" + Environment.NewLine;
autoResetEvent.WaitOne();
label1.Text += "我继续做一些工作,然后结束了!";
//省略工作代码
});
tWork1.IsBackground = true;
tWork1.Start();
}

private void StartThread2()
{
Thread tWork2 = new Thread(() =>
{
label2.Text = "线程2启动..." + Environment.NewLine;
label2.Text += "开始处理一些实际的工作" + Environment.NewLine;
//省略工作代码
label2.Text += "我开始等待别的线程给我信号,才愿意继续下去" + Environment.NewLine;
autoResetEvent.WaitOne();
label2.Text += "我继续做一些工作,然后结束了!";
//省略工作代码
});
tWork2.IsBackground = true;
tWork2.Start();
}

private void buttonSet_Click(object sender, EventArgs e)
{
//给在autoResetEvent上等待的线程一个信号
autoResetEvent.Set();
}

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

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

3:应用实例

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


AutoResetEvent autoResetEvent = new AutoResetEvent(false);

private void buttonStartAThread_Click(object sender, EventArgs e)
{
Thread tClient = new Thread(() =>
{
while (true)
{
//等3秒,3秒没有信号,显示断开
//有信号,则显示更新
bool re = autoResetEvent.WaitOne(3000);
if (re)
{
label1.Text = string.Format("时间:{0},{1}", DateTime.Now.ToString(), "保持连接状态");
}
else
{
label1.Text = string.Format("时间:{0},{1}", DateTime.Now.ToString(), "断开,需要重启");
}
}
});
tClient.IsBackground = true;
tClient.Start();
}

private void buttonSet_Click(object sender, EventArgs e)
{
//模拟发送心跳数据
autoResetEvent.Set();
}


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

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

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




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






之前的话题:

改善C#程序的建议5:引用类型赋值为null与加速垃圾回收

改善C#程序的建议4:C#中标准Dispose模式的实现

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

改善C#程序的建议2:C#中dynamic的正确用法

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

时间: 2024-07-30 16:03:14

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

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

原文:改善C#程序的建议7:正确停止线程 开发者总尝试对自己的代码有更多的控制."让那个还在工作的线程马上停止下来"就是诸多要求中的一种.然而事与愿违,这里面至少存在两个问题: 第一个问题是:正如线程不能立即启动一样,线程也并不能说停就停.无论采用何种方式通知工作线程需要停止,工作线程都会忙完手头最紧要的活,然后在它觉得合适的时候退出.以最传统的Thread.Abort方法为例,如果线程当前正在执行的是一段非托管代码,那么CLR就不会抛出ThreadAbortException,只有当

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

原文:改善C#程序的建议8:避免锁定不恰当的同步对象 在C#中让线程同步的另一种编码方式就是使用线程锁.所谓线程锁,就是锁住一个资源,使得应用程序只能在此刻有一个线程访问该资源.可以用下面这句不是那么贴切的话来理解线程锁的作用:锁,就是让多线程变成单线程.在C#中,可以将被锁定的资源理解成new出来的普通对象. 既然需要锁定的资源就是一个C#中的对象,我们就该仔细思考,到底什么样的对象能够成为一个锁对象(也叫同步对象)?在选择同步对象的时候,应当始终注意以下几点:     q同步对象在需要同步

改善C#程序的建议5:引用类型赋值为null与加速垃圾回收

原文:改善C#程序的建议5:引用类型赋值为null与加速垃圾回收 在标准的Dispose模式中(见前一篇博客"C#中标准Dispose模式的实现"),提到了需要及时释放资源,却并没有进一步细说让引用等于null是否有必要. 有一些人认为等于null可以帮助垃圾回收机制早点发现并标识对象是垃圾.其他人则认为这没有任何帮助.是否赋值为null的问题首先在方法的内部被人提起.现在,为了更好的阐述提出的问题,我们来撰写一个Winform窗体应用程序.如下: privatevoid button

改善C#程序的建议2:C#中dynamic的正确用法

原文:改善C#程序的建议2:C#中dynamic的正确用法 dynamic是FrameWork4.0的新特性.dynamic的出现让C#具有了弱语言类型的特性.编译器在编译的时候不再对类型进行检查,编译期默认dynamic对象支持你想要的任何特性.比如,即使你对GetDynamicObject方法返回的对象一无所知,你也可以像如下那样进行代码的调用,编译器不会报错:   dynamic dynamicObject = GetDynamicObject(); Console.WriteLine(d

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

原文:改善C#程序的建议1:非用ICloneable不可的理由 好吧,我承认,这是一个反标题,实际的情况是:我找不到一个非用ICloneable不可的理由.事实上,接口ICloneable还会带来误解,因为它只有一个Clone方法. 我们都知道,对象的拷贝分为:浅拷贝和深拷贝.ICloneable仅有一个Clone方法使我们无法从命名的角度去区分到底是哪个拷贝. 浅拷贝:将对象的字段复制到副本(新的对象)中,同时将字段的值也赋值过去,但是引用类型字段只复制引用,而不是引用类型本身.这意味着,源对

改善C#程序的建议4:C#中标准Dispose模式的实现

原文:改善C#程序的建议4:C#中标准Dispose模式的实现 需要明确一下C#程序(或者说.NET)中的资源.简单的说来,C#中的每一个类型都代表一种资源,而资源又分为两类: 托管资源:由CLR管理分配和释放的资源,即由CLR里new出来的对象: 非托管资源:不受CLR管理的对象,windows内核对象,如文件.数据库连接.套接字.COM对象等: 毫无例外地,如果我们的类型使用到了非托管资源,或者需要显式释放的托管资源,那么,就需要让类型继承接口IDisposable.这相当于是告诉调用者,该

编写高质量代码改善C#程序的建议:泛型集合、选择集合和集合的安全

前言 软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行. 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议20.使用泛型集合来替代非泛型集合 建议21.选择正确的集合 建议22.确保集合的线性安全 建议

线程同步中异常情况的处理

 本文主要用来说明多线程中异常情况的处理.       问题出现:使用Lock进行多线程中的同步的时候,如果在Lock块里面出现了异常,那么同步的资源(变量)就没有办法被释放,最终将导致线程死锁. 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace testLockList 8 {

基本线程同步(五)使用Lock同步代码块

声明:本文是< Java 7 Concurrency Cookbook >的第二章,作者: Javier Fernández González     译者:许巧辉 校对:方腾飞 使用Lock同步代码块 Java提供另外的机制用来同步代码块.它比synchronized关键字更加强大.灵活.它是基于Lock接口和实现它的类(如ReentrantLock).这种机制有如下优势: 它允许以一种更灵活的方式来构建synchronized块.使用synchronized关键字,你必须以结构化方式得到释