C#中的线程(二)线程同步
2016-06-29 11:43
246 查看
C#中的线程(二)线程同步
Keywords:C#线程Source:
Author:JoeAlbahari
Translator:SwankyWu
Published:
Download:
第二部分:线程同步基础
同步要领
下面的表格列展了.NET对协调或同步线程动作的可用的工具:简易阻止方法
构成 | 目的 |
阻止给定的时间周期 | |
等待另一个线程完成 |
构成 | 目的 | 跨进程? | 速度 |
确保只有一个线程访问某个资源或某段代码。 | 否 | 快 | |
确保只有一个线程访问某个资源或某段代码。 可被用于 | 中等 | ||
确保不超过指定数目的线程访问某个资源或某段代码。 | 是 | 中等 |
信号系统
构成 | 目的 | 跨进程? | 速度 |
允许线程等待直到它受到了另一个线程发出信号。 | 中等 | ||
允许一个线程等待直到自定义阻止条件得到满足。 | 否 | 中等 |
构成 | 目的 | 跨进程? | 速度 |
完成简单的非阻止原子操作。 | 是(内存共享情况下) | 非常快 | |
允许安全的非阻止在锁之外使用个别字段。 | 非常快 |
阻止(Blocking)
当一个线程通过上面所列的方式处于等待或暂停的状态,被称为被阻止。一旦被阻止,线程立刻放弃它被分配的CPU时间,将它的阻止的条件已得到满足
操作超时(如果timeout被指定了)
通过
通过
当线程通过(不建议)
休眠和轮询
调用Thread.Sleep阻止当前的线程指定的时间(或者直到staticvoidMain(){
Thread.Sleep(0);//释放CPU时间片
Thread.Sleep(1000);//休眠1000毫秒
Thread.Sleep(TimeSpan.FromHours(1));//休眠1小时
Thread.Sleep(Timeout.Infinite);//休眠直到中断
}
更确切地说,Thread.Sleep放弃了占用CPU,请求不在被分配时间直到给定的时间经过。Thread.Sleep(0)放弃CPU的时间刚刚够其它在时间片队列里的活动线程(如果有的话)被执行。
Thread.Sleep在阻止方法中是唯一的暂停汲取WindowsForms程序的Windows消息的方法,或COM环境中用于
线程类同时也提供了一个SpinWait方法,它使用轮询CPU而非放弃CPU时间的方式,保持给定的迭代次数进行“无用地繁忙”。50迭代可能等同于停顿大约一微秒,虽然这将取决于CPU的速度和负载。从技术上讲,SpinWait并不是一个阻止的方法:一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,并且也不会被其它的线程过早的中断(Interrupt)。SpinWait很少被使用,它的作用是等待一个在极短时间(可能小于一微秒)内可准备好的可预期的资源,而不用调用Sleep方法阻止线程而浪费CPU时间。不过,这种技术的优势只有在多处理器计算机:对单一处理器的电脑,直到轮询的线程结束了它的时间片之前,一个资源没有机会改变状态,这有违它的初衷。并且调用SpinWait经常会花费较长的时间这本身就浪费了CPU时间。
阻止vs.轮询
线程可以等待某个确定的条件来明确轮询使用一个轮询的方式,比如:while(!proceed);
或者:
while(DateTime.Now<nextStartTime);
这是非常浪费CPU时间的:对于CLR和操作系统而言,线程进行了一个重要的计算,所以分配了相应的资源!在这种状态下的轮询线程不算是阻止,不像一个线程等待一个
阻止和轮询组合使用可以产生一些变换:
while(!proceed)Thread.Sleep(x);//"轮询休眠!"
x越大,CPU效率越高,折中方案是增大潜伏时间,任何20ms的花费是微不足道的,除非循环中的条件是极其复杂的。
除了稍有延迟,这种轮询和休眠的方式可以结合的非常好。(但有并发问题,
使用Join等待一个线程完成
你可以通过Join方法阻止线程直到另一个线程结束:classJoinDemo{
staticvoidMain(){
Threadt=newThread(delegate(){Console.ReadLine();});
t.Start();
t.Join();//等待直到线程完成
Console.WriteLine("Threadt'sReadLinecomplete!");
}
}
Join方法也接收一个使用毫秒或用TimeSpan类的超时参数,当Join超时是返回false,如果线程已终止,则返回true。Join所带的超时参数非常像Sleep方法,实际上下面两行代码几乎差不多:
Thread.Sleep(1000);
Thread.CurrentThread.Join(1000);
(他们的区别明显在于单线程的应用程序域与COM互操作性,源于先前描述Windows信息汲取部分:在阻止时,Join保持信息汲取,Sleep暂停信息汲取。)
锁和线程安全
锁实现互斥的访问,被用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:classThreadUnsafe{
staticintval1,val2;
staticvoidGo(){
if(val2!=0)Console.WriteLine(val1/val2);
val2=0;
}
}
这不是
下面用lock来修正这个问题:
classThreadSafe{
staticobjectlocker=newobject();
staticintval1,val2;
staticvoidGo(){
lock(locker){
if(val2!=0)Console.WriteLine(val1/val2);
val2=0;
}
}
}
在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被
一个等候竞争锁的线程被阻止将在
C#的lock语句实际上是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法:
Monitor.Enter(locker);
try{
if(val2!=0)Console.WriteLine(val1/val2);
val2=0;
}
finally{Monitor.Exit(locker);}
在同一个对象上,在调用第一个之前Monitor.Enter而先调用了Monitor.Exit将引发异常。
Monitor也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false,因为超时了。TryEnter也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。
选择同步对象
任何对所有有关系的线程都可见的对象都可以作为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。服从这些规则,同步对象可以兼对象和保护两种作用。比如下面List:classThreadSafe{
List<string>list=newList<string>();
voidTest(){
lock(list){
list.Add("Item1");
...
一个专门字段是常用的(如在先前的例子中的locker),因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:
lock(this){...}
或:
lock(typeof(Widget)){...}//保护访问静态
是不好的,因为这潜在的可以在公共范围访问这些对象。
锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x)而被阻止,两者都要调用lock(x)来完成阻止工作。
嵌套锁定
线程可以重复锁定相同的对象,可以通过多次调用Monitor.Enter或lock语句来实现。当对应编号的Monitor.Exit被调用或最外面的lock语句完成后,对象那一刻被解锁。这就允许最简单的语法实现一个方法的锁调用另一个锁:staticobjectx=newobject();
staticvoidMain(){
lock(x){
Console.WriteLine("Ihavethelock");
Nest();
Console.WriteLine("Istillhavethelock");
}
在这锁被释放
}
staticvoidNest(){
lock(x){
...
}
释放了锁?没有完全释放!
}
线程只能在最开始的锁或最外面的锁时被阻止。
何时进行锁定
作为一项基本规则,任何和多线程有关的会进行读和写的字段应当加锁。甚至是极平常的事情——单一字段的赋值操作,都必须考虑到同步问题。在下面的例子中Increment和Assign都不是线程安全的:classThreadUnsafe{
staticintx;
staticvoidIncrement(){x++;}
staticvoidAssign(){x=123;}
}
下面是Increment和Assign线程安全的版本:
classThreadUnsafe{
staticobjectlocker=newobject();
staticintx;
staticvoidIncrement(){lock(locker)x++;}
staticvoidAssign(){lock(locker)x=123;}
}
作为锁定另一个选择,在一些简单的情况下,你可以使用
锁和原子操作
如果有很多变量在一些锁中总是进行读和写的操作,那么你可以称之为原子操作。我们假设x和y不停地读和赋值,他们在锁内通过locker锁定:lock(locker){if(x!=0)y/=x;}
你可以认为x和y通过原子的方式访问,因为代码段没有被其它的线程分开或抢占,别的线程改变x和y是无效的输出,你永远不会得到除数为零的错误,保证了x和y总是被相同的排他锁访问。
性能考量
锁定本身是非常快的,一个锁在没有堵塞的情况下一般只需几十纳秒(十亿分之一秒)。如果发生堵塞,任务切换带来的开销接近于数微秒(百万分之一秒)的范围内,尽管在线程重组实际的安排时间之前它可能花费数毫秒(千分之一秒)。而相反,与此相形见绌的是该使用锁而没使用的结果就是带来数小时的时间,甚至超时。如果耗尽并发,锁定会带来反作用,死锁和争用锁,耗尽并发由于太多的代码被放置到锁语句中了,引起其它线程不必要的被阻止。死锁是两线程彼此等待被锁定的内容,导致两者都无法继续下去。争用锁是两个线程任一个都可以锁定某个内容,如果“错误”的线程获取了锁,则导致程序错误。
对于太多的同步对象死锁是非常容易出现的症状,一个好的规则是开始于较少的锁,在一个可信的情况下涉及过多的阻止出现时,增加锁的粒度。
线程安全
线程安全的代码是指在面对任何多线程情况下,这代码都没有不确定的因素。线程安全首先完成锁,然后减少在线程间交互的可能性。一个线程安全的方法,在任何情况下可以可重入式调用。通用类型在它们中很少是线程安全的,原因如下:
完全线程安全的开发是重要的,尤其是一个类型有很多字段(在任意多线程上下文中每个字段都有潜在的交互作用)的情况下。
线程安全带来性能损失(要付出的,在某种程度上无论与否类型是否被用于多线程)。
一个线程安全类型不一定能使程序使用线程安全,有时参与工作后者可使前者变得冗余。
因此线程安全经常只在需要实现的地方来实现,为了处理一个特定的多线程情况。
不过,有一些方法来“欺骗”,有庞大和复杂的类安全地运行在多线程环境中。一种是牺牲粒度包含大段的代码——甚至在排他锁中访问全局对象,迫使在更高的级别上实现串行化访问。这一策略也很关键,让非线程安全的对象用于线程安全代码中,避免了相同的互斥锁被用于保护对在非线程安全对象的所有的属性、方法和字段的访问。
原始类型除外,很少的.NETframework类型实例相比于并发的只读访问,是线程安全的。责任在开放人员
另一个方式欺骗是通过最小化共享数据来最小化线程交互。这是一个很好的途径,被暗中地用于“弱状态”的中间层程序和web服务器。自多个客户端请求同时到达,每个请求来自它自己的线程(效力于ASP.NET,Web服务器或者远程体系结构),这意味着它们调用的方法一定是线程安全的。弱状态设计(因伸缩性好而流行)本质上限制了交互的能力,因此类不能够在每个请求间持久保留数据。线程交互仅限于可以被选择创建的静态字段,多半是在内存里缓存常用数据和提供基础设施服务,例如认证和审核。
线程安全与.NETFramework类型
锁定可被用于将非线程安全的代码转换成classThreadSafe{
staticList<string>list=newList<string>();
staticvoidMain(){
newThread(AddItems).Start();
newThread(AddItems).Start();
}
staticvoidAddItems(){
for(inti=0;i<100;i++)
lock(list)
list.Add("Item"+list.Count);
string[]items;
lock(list)items=list.ToArray();
foreach(stringsinitems)Console.WriteLine(s);
}
}
在这种情况下,我们锁定了list对象本身,这个简单的方案是很好的。如果我们有两个相关的list,也许我们就要锁定一个共同的目标——可能是单独的一个字段,如果没有其它的list出现,显然锁定它自己是明智的选择。
枚举.NET的集合也不是线程安全的,在枚举的时候另一个线程改动list的话,会抛出异常。胜于直接锁定枚举过程,在这个例子中,我们首先将项目复制到数组当中,这就避免了固定住锁因为我们在枚举过程中有潜在的耗时。
这里的一个有趣的假设:想象如果List实际上为线程安全的,如何解决呢?代码会很少!举例说明,我们说我们要增加一个项目到我们假象的线程安全的list里,如下:
if(!myList.Contains(newItem))myList.Add(newItem);
无论与否list是否为线程安全的,这个语句显然不是!整个if语句必须放到一个锁中,用来保护抢占在判断有无和增加新的之间。上述的锁需要用于任何我们需要修改list的地方,比如下面的语句需要被同样的锁包括住:
myList.Clear();
来保证它没有抢占之前的语句,换言之,我们必须锁定差不多所有非线程安全的集合类们。内置的线程安全,显而易见是浪费时间!
在写自定义组件的时候,你可能会反对这个观点——为什么建造线程安全让它容易的结果会变的多余呢?
有一个争论:在一个对象包上自定义的锁仅在所有并行的线程知道、并且使用这个锁的时候才能工作,而如果锁对象在更大的范围内的时候,这个锁对象可能不在这个锁范围内。最糟糕的情况是静态成员在公共类型中出现了,比如,想象静态结构在DateTime上,DateTime.Now不是线程安全的,当有2个并发的调用可带来错乱的输出或异常,补救方式是在其外进行锁定,可能锁定它的类型本身——lock(typeof(DateTime))来圈住调用DateTime.Now,这会工作的,但只有所有的程序员同意这样做的时候。然而这并靠不住,锁定一个类型被认为是一件非常不好的事情。
由于这些理由,DateTime上的静态成员是保证线程安全的,这是一个遍及.NETframework一个普遍模式——静态成员是线程安全的,而一个实例成员则不是。从这个模式也能在写自定义类型时得到一些体会,不要创建一个不能线程安全的难题!
当写公用组件的时候,好的习惯是不要忘记了线程安全,这意味着要单独小心处理那些在其中或公共的静态成员。
Interrupt和Abort
一个被阻止的线程可以通过两种方式被提前释放:通过
通过
这必须通过另外活动的线程实现,等待的线程是没有能力对它的被阻止状态做任何事情的。
Interrupt方法
在一个被阻止的线程上调用Interrupt方法,将强迫释放它,抛出ThreadInterruptedException异常,如下:classProgram{
staticvoidMain(){
Threadt=newThread(delegate(){
try{
Thread.Sleep(Timeout.Infinite);
}
catch(ThreadInterruptedException){
Console.Write("Forcibly");
}
Console.WriteLine("Woken!");
});
t.Start();
t.Interrupt();
}
}
ForciblyWoken!
中断一个线程仅仅释放它的当前的(或下一个)等待状态:它并不结束这个线程(当然,除非未处理ThreadInterruptedException异常)。
如果Interrupt被一个未阻止的线程调用,那么线程将继续执行直到下一次被阻止时,它抛出ThreadInterruptedException异常。用下面的测试避免这个问题:
if((worker.ThreadState&ThreadState.WaitSleepJoin)>0)
worker.Interrupt();
这不是一个
随意中断线程是危险的,因为任何框架或第三方方法在调用堆栈时可能会意外地在已订阅的代码上收到中断。这一切将被认为是线程被暂时阻止在一个锁中或同步资源中,并且所有挂起的中断将被踢开。如果这个方法没有被设计成可以被中断(没有适当处理finally块)的对象可能剩下无用的状态,或资源不完全地被释放。
中断一个线程是安全的,当你知道它确切的在哪的时候。稍后我们讨论
Abort方法
被阻止的线程也可以通过Abort方法被强制释放,这与调用Interrupt相似,除了用ThreadAbortException异常代替了ThreadInterruptedException异常,此外,异常将被重新抛出在catch里(在试图以有好方式处理异常的时候),直到Thread.ResetAbort在catch中被调用;在这期间线程的ThreadState为AbortRequested。在Interrupt与Abort之间最大不同在于它们调用一个非阻止线程所发生的事情。Interrupt继续工作直到下一次阻止发生,Abort在线程当前所执行的位置(可能甚至不在你的代码中)抛出异常。终止一个非阻止的线程会带来严重的后果,这在后面的“
线程状态
图1:线程状态关系图
你可以通过ThreadState属性获取线程的执行状态。图1将ThreadState列举为“层”。ThreadState被设计的很恐怖,它以按位计算的方式组合三种状态“层”,每种状态层的成员它们间都是互斥的,下面是所有的三种状态“层”:
运行(running)/阻止(blocking)/终止(aborting)状态(图1显示)
后台(background)/前台(foreground)状态(ThreadState.Background)
不建议使用的
总的来说,ThreadState是按位组合零或每个状态层的成员!一个简单的ThreadState例子:
Unstarted
Running
WaitSleepJoin
Background,Unstarted
SuspendRequested,Background,WaitSleepJoin
(所枚举的成员有两个从来没被用过,至少是当前CLR实现上:StopRequested和Aborted。)
还有更加复杂的,ThreadState.Running潜在的值为0,因此下面的测试不工作:
if((t.ThreadState&ThreadState.Running)>0)...
你必须用按位与非操作符来代替,或者使用线程的IsAlive属性。但是IsAlive可能不是你想要的,它在被阻止或挂起的时候返回true(只有在线程未开始或已结束时它才为true)。
假设你避开不推荐使用的Suspend和Resume方法,你可以写一个helper方法除去所有除了第一种状态层的成员,允许简单测试计算完成。线程的后台状态可以通过
publicstaticThreadStateSimpleThreadState(ThreadStatets)
{
returnts&(ThreadState.Aborted|ThreadState.AbortRequested|
ThreadState.Stopped|ThreadState.Unstarted|
ThreadState.WaitSleepJoin);
}
ThreadState对调试或程序概要分析是无价之宝,与之不相称的是多线程的协同工作,因为没有一个机制存在:通过判断ThreadState来执行信息,而不考虑ThreadState期间的变化。
等待句柄
Win32API拥有丰富的同步系统,这在.NETframework以EventWaitHandle,Mutex和Semaphore类展露出来。而一些比有些更有用:例如
这三个类都依赖于WaitHandle类,尽管从功能上讲,它们相当的不同。但它们做的事情都有一个共同点,那就是,被“点名”,这允许它们绕过操作系统进程工作,而不是只能在当前进程里绕过线程。
EventWaitHandle有两个子类:
性能方面,使用WaitHandles系统开销会花费在较小微秒间,不会在它们使用的上下文中产生什么后果。
AutoResetEvent在WaitHandle中是最有用的的类,它连同lock语句是一个主要的同步结构。
AutoResetEvent
AutoResetEvent就像一个用票通过的旋转门:插入一张票,让正确的人通过。类名字里的“auto”实际上就是旋转门自动关闭或“重新安排”后来的人让其通过。一个线程等待或如果Set调用时没有任何线程处于等待状态,那么句柄保持打开直到某个线程调用了WaitOne。这个行为避免了在线程起身去旋转门和线程插入票(哦,插入票是非常短的微秒间的事,真倒霉,你将必须不确定地等下去了!)间的竞争。但是在没人等的时候重复地在门上调用Set方法不会允许在一队人都通过,在他们到达的时候:仅有下一个人可以通过,多余的票都被“浪费了"。
WaitOne接受一个可选的超时参数——当等待以超时结束时这个方法将返回false,WaitOne在等待整段时间里也通知离开当前的
Reset方法提供在没有任何等待或阻止的时候关闭旋转门——它应该是开着的。
AutoResetEvent可以通过2种方式创建,第一种是通过构造函数:
EventWaitHandlewh=newAutoResetEvent(false);
如果布尔参数为真,Set方法在构造后立刻被自动的调用,另一个方法是通过它的基类EventWaitHandle:
EventWaitHandlewh=newEventWaitHandle(false,EventResetMode.Auto);
EventWaitHandle的构造器也允许创建
在WaitHandle不在需要时候,你应当调用Close方法来释放操作系统资源。但是,如果一个WaitHandle将被用于程序(就像这一节的大多例子一样)的生命周期中,你可以发点懒省略这个步骤,它将在程序域销毁时自动的被销毁。
接下来这个例子,一个线程开始等待直到另一个线程发出信号。
classBasicWaitHandle{
staticEventWaitHandlewh=newAutoResetEvent(false);
staticvoidMain(){
newThread(Waiter).Start();
Thread.Sleep(1000);//等一会...
wh.Set();//OK——唤醒它
}
staticvoidWaiter(){
Console.WriteLine("Waiting...");
wh.WaitOne();//等待通知
Console.WriteLine("Notified");
}
}
Waiting...(pause)Notified.
创建跨进程的EventWaitHandle
EventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的EventWaitHandle,除非操作系统创建一个新的,看这个例子:EventWaitHandlewh=newEventWaitHandle(false,EventResetMode.Auto,
"MyCompany.MyApp.SomeName");
如果有两个程序都运行这段代码,他们将彼此可以发送信号,等待句柄可以跨这两个进程中的所有线程。
任务确认
设想我们希望在后台完成任务,不在每次我们得到任务时再创建一个新的线程。我们可以通过一个轮询的线程来完成:等待一个任务,执行它,然后等待下一个任务。这是一个普遍的多线程方案。也就是在创建线程上切分内务操作,任务执行被序列化,在多个工作线程和过多的资源消耗间排除潜在的不想要的操作。我们必须决定要做什么,但是,如果当新的任务来到的时候,工作线程已经在忙之前的任务了,设想这种情形下我们需选择阻止调用者直到之前的任务被完成。像这样的系统可以用两个AutoResetEvent对象实现:一个“ready”AutoResetEvent,当准备好的时候,它被工作线程调用Set方法;和“go”AutoResetEvent,当有新任务的时候,它被调用线程调用Set方法。在下面的例子中,一个简单的string字段被用于决定任务(使用了
classAcknowledgedWaitHandle{
staticEventWaitHandleready=newAutoResetEvent(false);
staticEventWaitHandlego=newAutoResetEvent(false);
staticvolatilestringtask;
staticvoidMain(){
newThread(Work).Start();
//给工作线程发5次信号
for(inti=1;i<=5;i++){
ready.WaitOne();//首先等待,直到工作线程准备好了
task="a".PadRight(i,'h');//给任务赋值
go.Set();//告诉工作线程开始执行!
}
//告诉工作线程用一个null任务来结束
ready.WaitOne();task=null;go.Set();
}
staticvoidWork(){
while(true){
ready.Set();//指明我们已经准备好了
go.WaitOne();//等待被踢脱...
if(task==null)return;//优雅地退出
Console.WriteLine(task);
}
}
}
ah
ahh
ahhh
ahhhh
注意我们要给task赋null来告诉工作线程退出。在工作线程上调用
生产者/消费者队列
另一个普遍的线程方案是在后台工作进程从队列中分配任务。这叫做生产者/消费者队列:在工作线程中生产者入列任务,消费者出列任务。这和上个例子很像,除了当工作线程正忙于一个任务时调用者没有被阻止之外。生产者/消费者队列是可缩放的,因为多个消费者可能被创建——每个都服务于相同的队列,但开启了一个分离的线程。这是一个很好的方式利用多处理器的系统来限制工作线程的数量一直避免了极大的并发线程的缺陷(过多的内容切换和资源连接)。
在下面例子里,一个单独的AutoResetEvent被用于通知工作线程,它只有在用完任务时(队列为空)等待。一个通用的集合类被用于队列,必须通过锁控制它的访问以确保
usingSystem;
usingSystem.Threading;
usingSystem.Collections.Generic;
classProducerConsumerQueue:IDisposable{
EventWaitHandlewh=newAutoResetEvent(false);
Threadworker;
objectlocker=newobject();
Queue<string>tasks=newQueue<string>();
publicProducerConsumerQueue(){
worker=newThread(Work);
worker.Start();
}
publicvoidEnqueueTask(stringtask){
lock(locker)tasks.Enqueue(task);
wh.Set();
}
publicvoidDispose(){
EnqueueTask(null);//告诉消费者退出
worker.Join();//等待消费者线程完成
wh.Close();//释放任何OS资源
}
voidWork(){
while(true){
stringtask=null;
lock(locker)
if(tasks.Count>0){
task=tasks.Dequeue();
if(task==null)return;
}
if(task!=null){
Console.WriteLine("Performingtask:"+task);
Thread.Sleep(1000);//模拟工作...
}
else
wh.WaitOne();//没有任务了——等待信号
}
}
}
下面是一个主方法测试这个队列:
classTest{
staticvoidMain(){
using(ProducerConsumerQueueq=newProducerConsumerQueue()){
q.EnqueueTask("Hello");
for(inti=0;i<10;i++)q.EnqueueTask("Say"+i);
q.EnqueueTask("Goodbye!");
}
//使用using语句的调用q的Dispose方法,
//它入列一个null任务,并等待消费者完成
}
}
Performingtask:Hello
Performingtask:Say1
Performingtask:Say2
Performingtask:Say3
...
...
Performingtask:Say9
Goodbye!
注意我们明确的关闭了WaitHandle在ProducerConsumerQueue被销毁的时候,因为在程序的生命周期中我们可能潜在地创建和销毁许多这个类的实例。
ManualResetEvent
ManualResetEvent是你可以用一个布尔字段"gateOpen"(用
ManualResetEvent有时被用于给一个完成的操作发送信号,又或者一个已初始化正准备执行工作的线程。
互斥(Mutex)
Mutex提供了与C#的Mutex是相当快的,而lock又要比它快上数百倍,获取Mutex需要花费几微秒,获取lock需花费数十纳秒(假定没有阻止)。
对于一个Mutex类,WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex之后被释放,就像C#的lock语句一样,Mutex只能从获取互斥锁的这个线程上被释放。
Mutex在跨进程的普遍用处是确保在同一时刻只有一个程序的的实例在运行,下面演示如何使用:
classOneAtATimePlease{
//使用一个应用程序的唯一的名称(比如包括你公司的URL)
staticMutexmutex=newMutex(false,"oreilly.comOneAtATimeDemo");
staticvoidMain(){
//等待5秒如果存在竞争——存在程序在
//进程中的的另一个实例关闭之后
if(!mutex.WaitOne(TimeSpan.FromSeconds(5),false)){
Console.WriteLine("Anotherinstanceoftheappisrunning.Bye!");
return;
}
try{
Console.WriteLine("Running-pressEntertoexit");
Console.ReadLine();
}
finally{mutex.ReleaseMutex();}
}
}
Mutex有个好的特性是,如果程序结束时而互斥锁没通过ReleaseMutex首先被释放,CLR将自动地释放Mutex。
Semaphore
Semaphore就像一个夜总会:它有固定的容量,这由保镖来保证,一旦它满了就没有任何人可以再进入这个夜总会,并且在其外会形成一个队列。然后,当人一个人离开时,队列头的人便可以进入了。构造器需要至少两个参数——夜总会的活动的空间,和夜总会的容量。Semaphore的特性与Mutex和lock有点类似,除了Semaphore没有“所有者”——它是不可知线程的,任何在Semaphore内的线程都可以调用Release,而Mutex和lock仅有那些获取了资源的线程才可以释放它。
在下面的例子中,10个线程执行一个循环,在中间使用Sleep语句。Semaphore确保每次只有不超过3个线程可以执行Sleep语句:
classSemaphoreTest{
staticSemaphores=newSemaphore(3,3);//Available=3;Capacity=3
staticvoidMain(){
for(inti=0;i<10;i++)newThread(Go).Start();
}
staticvoidGo(){
while(true){
s.WaitOne();
Thread.Sleep(100);//每次只有3个线程可以到达这里
s.Release();
}
}
}
WaitAny,WaitAll和SignalAndWait
除了Set和WaitOne方法外,在类WaitHandle中还有一些用来创建复杂的同步过程的静态方法。WaitAny,WaitAll和SignalAndWait使跨多个可能为不同类型的等待句柄变得容易。
SignalAndWait可能是最有用的了:他在某个WaitHandle上调用WaitOne,并在另一个WaitHandle上自动地调用Set。你可以在一对EventWaitHandle上装配两个线程,而让它们在某个时间点“相遇”,这马马虎虎地合乎规范。AutoResetEvent或ManualResetEvent都无法使用这个技巧。第一个线程像这样:
WaitHandle.SignalAndWait(wh1,wh2);
同时第二个线程做相反的事情:
WaitHandle.SignalAndWait(wh2,wh1);
WaitHandle.WaitAny等待一组等待句柄任意一个发出信号,WaitHandle.WaitAll等待所有给定的句柄发出信号。与票据旋转门的例子类似,这些方法可能同时地等待所有的旋转门——通过在第一个打开的时候(WaitAny情况下),或者等待直到它们所有的都打开(WaitAll情况下)。
WaitAll实际上是不确定的值,因为这与
幸运地是,在等待句柄难使用或不适合的时候,.NETframework提供了更先进的信号结构——
同步环境
与手工的锁定相比,你可以进行说明性的锁定,用衍生自ContextBoundObject并标以Synchronization特性的类,它告诉CLR自动执行锁操作,看这个例子:usingSystem;
usingSystem.Threading;
usingSystem.Runtime.Remoting.Contexts;
[Synchronization]
publicclassAutoLock:ContextBoundObject{
publicvoidDemo(){
Console.Write("Start...");
Thread.Sleep(1000);//我们不能抢占到这
Console.WriteLine("end");//感谢自动锁!
}
}
publicclassTest{
publicstaticvoidMain(){
AutoLocksafeInstance=newAutoLock();
newThread(safeInstance.Demo).Start();//并发地
newThread(safeInstance.Demo).Start();//调用Demo
safeInstance.Demo();//方法3次
}
}
Start...end
Start...end
Start...end
CLR确保了同一时刻只有一个线程可以执行safeInstance中的代码。它创建了一个同步对象来完成工作,并在每次调用safeInstance的方法和属性时在其周围只能够行锁定。锁的作用域——这里是safeInstance对象,被称为同步环境。
那么,它是如何工作的呢?Synchronization特性的命名空间:System.Runtime.Remoting.Contexts是一个线索。ContextBoundObject可以被认为是一个“远程”对象,这意味着所有方法的调用是被监听的。让这个监听称为可能,就像我们的例子AutoLock,CLR自动的返回了一个具有相同方法和属性的AutoLock对象的代理对象,它扮演着一个中间者的角色。总的来说,监听在每个方法调用时增加了数微秒的时间。
自动同步不能用于静态类型的成员,和非继承自ContextBoundObject(例如:WindowsForm)的类。
锁在内部以相同的方式运作,你可能期待下面的例子与之前的有一样的结果:
[Synchronization]
publicclassAutoLock:ContextBoundObject{
publicvoidDemo(){
Console.Write("Start...");
Thread.Sleep(1000);
Console.WriteLine("end");
}
publicvoidTest(){
newThread(Demo).Start();
newThread(Demo).Start();
newThread(Demo).Start();
Console.ReadLine();
}
publicstaticvoidMain(){
newAutoLock().Test();
}
}
(注意我们放入了Console.ReadLine语句。)因为在同一时刻的同一个此类的对象中只有一个线程可以执行代码,三个新线程将保持被阻止在Demo放中,直到Test方法完成,需要等待ReadLine来完成。因此我们以与之前的有相同结果而告终,但是只有在按完Enter键之后。这是一个线程安全的手段,差不多足够能在类中排除任何有用的多线程!
此外,我们仍未解决之前描述的一个问题:如果AutoLock是一个集合类,比如说,我们仍然需要一个像下面一样的锁,假设运行在另一个类里:
if(safeInstance.Count>0)safeInstance.RemoveAt(0);
除非使用这代码的类本身是一个同步的ContextBoundObject!
同步环境可以扩展到超过一个单独对象的区域。默认地,如果一个同步对象被实例化从在另一段代码之内,它们拥有共享相同的同步环境(换言之,一个大锁!)。这个行为可以由改变Synchronization特性的构造器的参数来指定。使用SynchronizationAttribute类定义的常量之一:
常量 | 含义 |
NOT_SUPPORTED | 相当于不使用同步特性 |
SUPPORTED | 如果从另一个同步对象被实例化,则合并已存在的同步环境,否则只剩下非同步。 |
REQUIRED (默认) | 如果从另一个同步对象被实例化,则合并已存在的同步环境,否则创建一个新的同步环境。 |
REQUIRES_NEW | 总是创建新的同步环境 |
[Synchronization(SynchronizationAttribute.REQUIRES_NEW)]
publicclassSynchronizedB:ContextBoundObject{...
越大的同步环境越容易管理,但是减少机会对有用的并发。换个有限的角度,分离的同步环境会造成死锁,看这个例子:
[Synchronization]
publicclassDeadlock:ContextBoundObject{
publicDeadLockOther;
publicvoidDemo(){Thread.Sleep(1000);Other.Hello();}
voidHello(){Console.WriteLine("hello");}
}
publicclassTest{
staticvoidMain(){
Deadlockdead1=newDeadlock();
Deadlockdead2=newDeadlock();
dead1.Other=dead2;
dead2.Other=dead1;
newThread(dead1.Demo).Start();
dead2.Demo();
}
}
因为每个Deadlock的实例在Test内创建——一个非同步类,每个实例将有它自己的同步环境,因此,有它自己的锁。当它们彼此调用的时候,不会花太多时间就会死锁(确切的说是一秒!)。如果Deadlock和Test是由不同开发团队来写的,这个问题特别容易发生。别指望Test知道如何产生的错误,更别指望他们来解决它了。在死锁显而易见的情况下,这与使用明确的锁的方式形成鲜明的对比。
可重入性问题
不过在自动锁方式上,如果Synchronization的参数可重入式的为true的话,可重入性会有潜在的问题:
[Synchronization(true)]
同步环境的锁在执行离开上下文时被临时地释放。在之前的例子里,这将能预防死锁的发生;很明显很需要这样的功能。然而一个副作用是,在这期间,任何线程都可以自由的调用在目标对象(“重进入”的同步上下文)的上任何方法,而非常复杂的多线程中试图避免不释放资源是排在首位的。这就是可重入性的问题。
因为[Synchronization(true)]作用于类级别,这特性打开了对于非上下文的方法访问,由于可重入性问题使它们混入类的调用。
虽然可重入性是危险的,但有些时候它是不错的选择。比如:设想一个在其内部实现多线程同步的类,将逻辑工作线程运行在不同的语境中。在没有可重入性问题的情况下,工作线程在它们彼此之间或目标对象之间可能被无理地阻碍。
这凸显了自动同步的一个基本弱点:超过适用的大范围的锁定带来了其它情况没有带来的巨大麻烦。这些困难:死锁,可重入性问题和被阉割的并发,使另一个更简单的方案——手动的锁定变得更为合适。
相关文章推荐
- C#中哈希表(HashTable)用法实例详解(添加/移除/判断/遍历/排序等)
- C#使用foreach循环遍历数组完整实例
- c#传入域名返回对应的IP
- c#获取WebBrowser中的userAgent
- c#geckofx文件流下载
- C#遍历文件夹及其子目录的完整实现方法
- C# 验证数字
- C#简单遍历指定文件夹中所有文件的方法
- c#读取Excel的第一个Sheet表
- C#如何读取Excel文件
- C#基础一
- C#中一些常用的加密和哈希处理
- C#个人总结2——类和对象
- C#使用tesseract3.02识别验证码模拟登录
- c#中Invoke使用方法(线程间操作无效: 从不是创建控件的线程访问它。那么创建它的线程是哪个线程?)
- C#.NET 定时器类及使用方法
- LCS算法找出两个字符串最长公共子串(C#实现)
- c# PictureBox 的图像上使用鼠标画矩形框
- 单例(C#版)
- C#执行存储过程