第二部分:线程同步基础2
2009-07-21 14:22
309 查看
2008-05-1101:08
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语句是一个主要的同步结构。 AutoResetEventAutoResetEvent就像一个用票通过的旋转门:插入一张票,让正确的人通过。类名字里的“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. 创建跨进程的EventWaitHandleEventWaitHandle的构造器允许以“命名”的方式进行创建,它有能力跨多个进程。名称是个简单的字符串,可能会无意地与别的冲突!如果名字使用了,你将引用相同潜在的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被销毁的时候,因为在程序的生命周期中我们可能潜在地创建和销毁许多这个类的实例。 ManualResetEventManualResetEvent是你可以用一个布尔字段"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。 SemaphoreSemaphore就像一个夜总会:它有固定的容量,这由保镖来保证,一旦它满了就没有任何人可以再进入这个夜总会,并且在其外会形成一个队列。然后,当人一个人离开时,队列头的人便可以进入了。构造器需要至少两个参数——夜总会的活动的空间,和夜总会的容量。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提供了更先进的信号结构—— |
相关文章推荐
- 第二部分:线程同步基础3
- [转]C#中的多线程-第二部分:线程同步基础
- C#中的多线程-第二部分:线程同步基础
- [转]C#中的多线程-第二部分:线程同步基础
- C#中的多线程 -第二部分:线程同步基础
- 第二部分:线程同步基础1
- 第二部分面向对像基础第五章
- 第二部分面向对像基础第五章下半部分与习题总结
- 《C++捷径教程》读书笔记--Chapter 7--函数,第一部分:基础知识(第二部分)
- Java EE 7 教程 第二部分 平台基础 第3章 创建资源 第3.1节 资源和JNDI命名
- java基础部分-《第二部分》-异常机…
- 【译】jquery基础教程(jQuery Fundamentals)——(第二部分)javascript基础
- 第二部分面向对像基础第五章
- 第二部分面向对像基础第五章Strng类中方法的使用
- 第二部分面向对像基础第五章下半部分与习题总结
- <香港科技大学html+css+js课堂笔记>week3--DOM模型基础第二部分
- 11-18网页基础--第二部分CSS样式属性(1)
- [置顶] 信息学奥赛一本通(C++版) 第二部分 基础算法 第九章 动态规划
- Android4开发入门经典 之 第二部分:Android应用的核心基础
- JAVA密码技术基础(第二部分)