c#多线程编程教学——线程同步
随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问。
考虑一种我们经常遇到的情况:有一些全局变量和共享的类变量,我们需要从不同的线程来更新它们,可以通过使用System.Threading.Interlocked类完成这样的任务,它提供了原子的,非模块化的整数更新操作。
还有你可以使用System.Threading.Monitor类锁定对象的方法的一段代码,使其暂时不能被别的线程访问。
System.Threading.WaitHandle类的实例可以用来封装等待对共享资源的独占访问权的操作系统特定的对象。
尤其对于非受管代码的互操作问题。
System.Threading.Mutex用于对多个复杂的线程同步的问题,它也允许单线程的访问。
像ManualResetEvent和AutoResetEvent这样的同步事件类支持一个类通知其他事件的线程。
不讨论线程的同步问题,等于对多线程编程知之甚少,但是我们要十分谨慎的使用多线程的同步。
在使用线程同步时,我们事先就要要能够正确的确定是那个对象和方法有可能造成死锁(死锁就是所有的线程都停止了相应,都在等者对方释放资源)。
还有赃数据的问题(指的是同一时间多个线程对数据作了操作而造成的不一致),这个不容易理解,这么说吧,有X和Y两个线程,线程X从文件读取数据并且写数据到数据结构,线程Y从这个数据结构读数据并将数据送到其他的计算机。
假设在Y读数据的同时,X写入数据,那么显然Y读取的数据与实际存储的数据是不一致的。
这种情况显然是我们应该避免发生的。
少量的线程将使得刚才的问题发生的几率要少的多,对共享资源的访问也更好的同步。
.NET Framework的CLR提供了三种方法来完成对共享资源 ,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。
(1) 代码域同步:使用Monitor类可以同步静态/实例化的方法的全部代码或者部分代码段。
不支持静态域的同步。
在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步,这在后面会讲到。
(2) 手工同步:使用不同的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)创建自己的同步机制。
这种同步方式要求你自己手动的为不同的域和方法同步,这种同步方式也可以用于进程间的同步和对共享资源的等待而造成的死锁解除。
(3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象创建简单的,自动的同步。
这种同步方式仅用于实例化的方法和域的同步。
所有在同一个上下文域的对象共享同一个锁。
Monitor Class
在给定的时间和指定的代码段只能被一个线程访问,Monitor 类非常适合于这种情况的线程同步。
这个类中的方法都是静态的,所以不需要实例化这个类。
下面一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。
Monitor.Enter 方法:在指定对象上获取排他锁。
Monitor.TryEnter 方法:试图获取指定对象的排他锁。
Monitor.Exit 方法:释放指定对象上的排他锁。
Monitor.Wait 方法:释放对象上的锁并阻塞当前线程,直到它重新获取该锁。
Monitor.Pulse 方法:通知等待队列中的线程锁定对象状态的更改。
Monitor.PulseAll 方法:通知所有的等待线程对象状态的更改。
通过对指定对象的加锁和解锁可以同步代码段的访问。
Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用来对指定对象的加锁和解锁。
一旦获取(调用了Monitor.Enter)指定对象(代码段)的锁,其他的线程都不能获取该锁。
举个例子来说吧,线程X获得了一个对象锁,这个对象锁可以释放的(调用Monitor.Exit(object) or Monitor.Wait)。
当这个对象锁被释放后,Monitor.Pulse方法和 Monitor.PulseAll方法通知就绪队列的下一个线程进行和其他所有就绪队列的线程将有机会获取排他锁。
线程X释放了锁而线程Y获得了锁,同时调用Monitor.Wait的线程X进入等待队列。
当从当前锁定对象的线程(线程Y)受到了Pulse或PulseAll,等待队列的线程就进入就绪队列。
线程X重新得到对象锁时,Monitor.Wait才返回。
如果拥有锁的线程(线程Y)不调用Pulse或PulseAll,方法可能被不确定的锁定。
Pulse, PulseAll and Wait必须是被同步的代码段鄂被调用。
对每一个同步的对象,你需要有当前拥有锁的线程的指针,就绪队列和等待队列(包含需要被通知锁定对象的状态变化的线程)的指针。
你也许会问,当两个线程同时调用Monitor.Enter会发生什么事情?无论这两个线程地调用Monitor.Enter是多么地接近,实际上肯定有一个在前,一个在后,因此永远只会有一个获得对象锁。
既然Monitor.Enter是原子操作,那么CPU是不可能偏好一个线程而不喜欢另外一个线程的。
为了获取更好的性能,你应该延迟后一个线程的获取锁调用和立即释放前一个线程的对象锁。
对于private和internal的对象,加锁是可行的,但是对于external对象有可能导致死锁,因为不相关的代码可能因为不同的目的而对同一个对象加锁。
如果你要对一段代码加锁,最好的是在try语句里面加入设置锁的语句,而将Monitor.Exit放在finally语句里面。
对于整个代码段的加锁,你可以使用MethodImplAttribute(在System.Runtime.CompilerServices命名空间)类在其构造器中设置同步值。
这是一种可以替代的方法,当加锁的方法返回时,锁也就被释放了。
如果需要要很快释放锁,你可以使用Monitor类和C# lock的声明代替上述的方法。
让我们来看一段使用Monitor类的代码:
public void some_method()
{
int a=100;
int b=0;
Monitor.Enter(this);
//say we do something here.
int c=a/b;
Monitor.Exit(this);
}
上面的代码运行会产生问题。
当代码运行到int c=a/b; 的时候,会抛出一个异常,Monitor.Exit将不会返回。
因此这段程序将挂起,其他的线程也将得不到锁。
有两种方法可以解决上面的问题。
第一个方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后一定会释放锁。
第二种方法是:利用C#的lock()方法。
调用这个方法和调用Monitoy.Enter的作用效果是一样的。
但是这种方法一旦代码执行超出范围,释放锁将不会自动的发生。
见下面的代码:
public void some_method()
{
int a=100;
int b=0;
lock(this);
//say we do something here.
int c=a/b;
}
C# lock申明提供了与Monitoy.Enter和Monitoy.Exit同样的功能,这种方法用在你的代码段不能被其他独立的线程中断的情况。
WaitHandle Class
WaitHandle类作为基类来使用的,它允许多个等待操作。
这个类封装了win32的同步处理方法。
WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。
下面是从它继承来的几个类:
Mutex 类:同步基元也可用于进程间同步。
AutoResetEvent:通知一个或多个正在等待的线程已发生事件。
无法继承此类。
ManualResetEvent:当通知一个或多个正在等待的线程事件已发生时出现。
无法继承此类。
这些类定义了一些信号机制使得对资源排他性访问的占有和释放。
他们有两种状态:signaled 和 nonsignaled。
Signaled状态的等待句柄不属于任何线程,除非是nonsignaled状态。
拥有等待句柄的线程不再使用等待句柄时用set方法,其他的线程可以调用Reset方法来改变状态或者任意一个WaitHandle方法要求拥有等待句柄,这些方法见下面:
WaitAll:等待指定数组中的所有元素收到信号。
WaitAny:等待指定数组中的任一元素收到信号。
WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。
这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。
WaitHandle对象封装等待对共享资源的独占访问权的操作系统特定的对象无论是收管代码还是非受管代码都可以使用。
但是它没有Monitor使用轻便,Monitor是完全的受管代码而且对操作系统资源的使用非常有效率。
Mutex Class
Mutex是另外一种完成线程间和跨进程同步的方法,它同时也提供进程间的同步。
它允许一个线程独占共享资源的同时阻止其他线程和进程的访问。
Mutex的名字就很好的说明了它的所有者对资源的排他性的占有。
一旦一个线程拥有了Mutex,想得到Mutex的其他线程都将挂起直到占有线程释放它。
Mutex.ReleaseMutex方法用于释放Mutex,一个线程可以多次调用wait方法来请求同一个Mutex,但是在释放Mutex的时候必须调用同样次数的Mutex.ReleaseMutex。
如果没有线程占有Mutex,那么Mutex的状态就变为signaled,否则为nosignaled。
一旦Mutex的状态变为signaled,等待队列的下一个线程将会得到Mutex。
Mutex类对应与win32的CreateMutex,创建Mutex对象的方法非常简单,常用的有下面几种方法:
一个线程可以通过调用WaitHandle.WaitOne 或 WaitHandle.WaitAny 或 WaitHandle.WaitAll得到Mutex的拥有权。
如果Mutex不属于任何线程,上述调用将使得线程拥有Mutex,而且WaitOne会立即返回。
但是如果有其他的线程拥有Mutex,WaitOne将陷入无限期的等待直到获取Mutex。
你可以在WaitOne方法中指定参数即等待的时间而避免无限期的等待Mutex。
调用Close作用于Mutex将释放拥有。
一旦Mutex被创建,你可以通过GetHandle方法获得Mutex的句柄而给WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。
下面是一个示例:
public void some_method()
{
int a=100;
int b=20;
Mutex firstMutex = new Mutex(false);
FirstMutex.WaitOne();
//some kind of processing can be done here.
Int x=a/b;
FirstMutex.Close();
}
在上面的例子中,线程创建了Mutex,但是开始并没有申明拥有它,通过调用WaitOne方法拥有Mutex。
Synchronization Events
同步时间是一些等待句柄用来通知其他的线程发生了什么事情和资源是可用的。
他们有两个状态:signaled and nonsignaled。
AutoResetEvent 和 ManualResetEvent就是这种同步事件。
AutoResetEvent Class
这个类可以通知一个或多个线程发生事件。
当一个等待线程得到释放时,它将状态转换为signaled。
用set方法使它的实例状态变为signaled。
但是一旦等待的线程被通知时间变为signaled,它的转台将自动的变为nonsignaled。
如果没有线程侦听事件,转台将保持为signaled。
此类不能被继承。
ManualResetEvent Class
这个类也用来通知一个或多个线程事件发生了。
它的状态可以手动的被设置和重置。
手动重置时间将保持signaled状态直到ManualResetEvent.Reset设置其状态为nonsignaled,或保持状态为nonsignaled直到ManualResetEvent.Set设置其状态为signaled。
这个类不能被继承。
Interlocked Class
它提供了在线程之间共享的变量访问的同步,它的操作时原子操作,且被线程共享.你可以通过Interlocked.Increment 或 Interlocked.Decrement来增加或减少共享变量.它的有点在于是原子操作,也就是说这些方法可以代一个整型的参数增量并且返回新的值,所有的操作就是一步.你也可以使用它来指定变量的值或者检查两个变量是否相等,如果相等,将用指定的值代替其中一个变量的值.
ReaderWriterLock class
它定义了一种锁,提供唯一写/多读的机制,使得读写的同步.任意数目的线程都可以读数据,数据锁在有线程更新数据时将是需要的.读的线程可以获取锁,当且仅当这里没有写的线程.当没有读线程和其他的写线程时,写线程可以得到锁.因此,一旦writer-lock被请求,所有的读线程将不能读取数据直到写线程访问完毕.它支持暂停而避免死锁.它也支持嵌套的读/写锁.支持嵌套的读锁的方法是ReaderWriterLock.AcquireReaderLock,如果一个线程有写锁则该线程将暂停;
支持嵌套的写锁的方法是ReaderWriterLock.AcquireWriterLock,如果一个线程有读锁则该线程暂停.如果有读锁将容易倒是死锁.安全的办法是使用ReaderWriterLock.UpgradeToWriterLock方法,这将使读者升级到写者.你可以用ReaderWriterLock.DowngradeFromWriterLock方法使写者降级为读者.调用ReaderWriterLock.ReleaseLock将释放锁, ReaderWriterLock.RestoreLock将重新装载锁的状态到调用ReaderWriterLock.ReleaseLock以前.
结论:
这部分讲述了.NET平台上的线程同步的问题.造接下来的系列文章中我将给出一些例子来更进一步的说明这些使用的方法和技巧.虽然线程同步的使用会给我们的程序带来很大的价值,但是我们最好能够小心使用这些方法.否则带来的不是受益,而将倒是性能下降甚至程序崩溃.只有大量的联系和体会才能使你驾驭这些技巧.尽量少使用那些在同步代码块完成不了或者不确定的阻塞的东西,尤其是I/O操作;尽可能的使用局部变量来代替全局变量;同步用在那些部分代码被多个线程和进程访问和状态被不同的进程共享的地方;安排你的代码使得每一个数据在一个线程里得到精确的控制;不是共享在线程之间的代码是安全的
一、题目: 创建线程,利用互斥实现线程共享变量通信
二、目的
掌握线程创建和终止,加深对线程和进程概念的理解,会用同步与互斥方法实现线程之间的通信。
三、内容和要求
软件界面上点“创建线程” 按钮,创建三个生产者线程(P1,P2,P3)和两个消费者线程(C1,C2),生产者和消费者线程共享一个长度为2KB的环型公共缓冲区,生产者向其中投放消息,消费者从中取走消息。
只要缓冲区未满,生产者可将消息送入缓冲区;只要缓冲区未空,消费者可从缓冲区取走一个消息。
每个消息具下列结构格式:
消息头(1B,固定为0xaa),消息长度(1B),消息内容(nB),校验和(1B),检验和计算方式为消息长度和消息内容所有字节异或结果。
每个生产者每隔n毫秒(n用随机数产生,1到100毫秒之间,间隔不固定)生产一个消息加入缓冲区,并把消息产生时间和内容记录在一个文本文件中(或显示在列表框中)。
P1每次生产的数据为26个大写字母, P2每次生产的数据为26个小写字母,P3每次生产的数据为10个数字。
每个消费者每隔n秒(n用随机数产生,1到5秒之间,间隔不固定)从缓冲区取走一个消息。
每消费一个消息需要将消费时间和消息内容记录在一个文本文件中(或显示在列表框中)。
当用户按结束按钮时结束5个线程,并将5个文件内容显示出来进行对照。
实验要求:
每人完成一份大作业实验报告。
报告分问题概述、设计思想、数据定义、处理流程、源程序、运行结果、设计体会等部分。
1) 概述所采用的同步方法;
2) 给出数据定义和详细说明;
3) 给出实现思想和设计流程;
4) 调试完成源程序;
5) 屏幕观察运行结果;
6) 总结自己的设计体会;
编程工具及操作系统平台不限,建议用VC6. 0或Delphi开发。
四、参考资料
Windows系统相关知识介绍
1. 同步对象
同步对象是指Windows中用于实现同步与互斥的实体,包括信号量(Semaphore)、互斥量(Mutex)、临界区(Critical Section)和事件(Events)等。
同步对象的使用步骤:
l 创建/初始化同步对象。
l 请求同步对象,进入临界区(互斥量上锁)。
l 释放同步对象(互斥量解锁)。
这些对象在一个线程中创建,在其他线程中都可以使用,实现同步与互斥。
本实验中需要用到互斥量(Mutex),用于多个线程对共享数据互斥访问。
2. 相关API的功能及使用
我们利用Windows SDK提供的API编程实现实验题目要求,而VC中包含有Windows SDK的所有工具和定义(参见MSDN)。
要使用这些API,需要包含这些函数进行说明的SDK头文件——最常见的是Windows.h(特殊的API调用还需要包含其他头文件)。
下面给出的是本实验使用到的API的功能和使用方法简单介绍。
(1) CreateThread
l 功能——创建一个在调用进程的地址空间中执行的线程
l 格式
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParamiter,
DWORD dwCreationFlags,
Lpdword lpThread );
l 参数说明
lpThreadAttributes——指向一个LPSECURITY_ATTRIBUTES(新线程的安全性描述符)。
dwStackSize——定义原始堆栈大小。
lpStartAddress——指向使用LPTHRAED_START_ROUTINE类型定义的函数。
lpParamiter——定义一个给进程传递参数的指针。
dwCreationFlags——定义控制线程创建的附加标志。
lpThread——保存线程标志符(32位)
(2) CreateMutex
l 功能——创建一个命名或匿名的互斥量对象
l 格式
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName);
l 参数说明
lpMutexAttributes——必须取值NULL。
bInitialOwner——指示当前线程是否马上拥有该互斥量(即马上加锁)。
lpName——互斥量名称。
(3) WaitForSingleObject
l 功能——使程序处于等待状态,直到信号量hHandle出现(即其值大于等于1)或超过规定的等待时间
l 格式
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
l 参数说明
hHandle——信号量指针。
dwMilliseconds——等待的最长时间(INFINITE为无限等待)。
(4) ReleaseMutex
l 功能——打开互斥锁,即把互斥量加1。
成功调用则返回0
l 格式
BOOL ReleaseMutex(HANDLE hMutex);
l 参数说明
hMutex——互斥量指针。
(5)其它有关函数Sleep,ResumeThread, TerminateThread,CloseHandle等,请查阅MSDN
五、提交内容
本大作业每个人必须单独完成。
最后需提交的内容包括:源程序(关键代码需要注释说明)、可运行程序、算法思路及流程图、心得体会。
将以上内容刻入光盘,光盘上写明班级、学号、姓名信息,再将大作业要求、源程序及注释、算法思路及流程图、心得体会等打印出来。
最后将打印稿及光盘统一交给网络学院教务员。
截止时间2009年12月1日。
过期自负。
大作业严禁抄袭。
发现抄袭一律以不及格论。
看到群里网友们在讨论由于不清楚的原因,有同学的网站无法访问。他的网站是没有用HTTPS的,直接访问他的HTTP是无法访问的,通过PING测试可以看到解析地址已经比较乱,应该是所谓的DNS污染。其中有网友提到采用HTTPS加密证书试试。因为HTTP和HTTPS走的不是一个端口,之前有网友这样测试过是可以缓解这样的问题。这样通过将网站绑定设置HTTPS之后,是可以打开的,看来网站的80端口出现问题,而...
BuyVM在昨天宣布上线了第四个数据中心产品:迈阿密,基于KVM架构的VPS主机,采用AMD Ryzen 3900X CPU,DDR4内存,NVMe硬盘,1Gbps带宽,不限制流量方式,最低$2/月起,支持Linux或者Windows操作系统。这是一家成立于2010年的国外主机商,提供基于KVM架构的VPS产品,数据中心除了新上的迈阿密外还包括美国拉斯维加斯、新泽西和卢森堡等,主机均为1Gbps带...
轻云互联成立于2018年的国人商家,广州轻云互联网络科技有限公司旗下品牌,主要从事VPS、虚拟主机等云计算产品业务,适合建站、新手上车的值得选择,香港三网直连(电信CN2GIA联通移动CN2直连);美国圣何塞(回程三网CN2GIA)线路,所有产品均采用KVM虚拟技术架构,高效售后保障,稳定多年,高性能可用,网络优质,为您的业务保驾护航。官方网站:点击进入广州轻云网络科技有限公司活动规则:1.用户购...