线程讲解c篇第二讲

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

C#中的线程(二)线程同步基础1.同步要领下面的表格列展了.NET对协调或同步线程动作的可用的工具:简易阻止方法构成目的Sleep阻止给定的时间周期Join等待另一个线程完成锁系统构成目的跨进程?速度lock确保只有一个线程访问某个资源或某段代码。否快Mutex确保只有一个线程访问某个资源或某段代码。可被用于防止一个程序的多个实例同时运行。是中等Semaphore确保不超过指定数目的线程访问某个资源或某段代码。是中等(同步的情况下也提够自动锁。)信号系统构成目的跨进程?速度EventWaitHandle允许线程等待直到它受到了另一个线程发出信号。是中等Wait和Pulse*允许一个线程等待直到自定义阻止条件得到满足。否中等非阻止同步系统*构成目的跨进程?速度Interlocked*完成简单的非阻止原子操作。是(内存共享情况下)非常快volatile*允许安全的非阻止在锁之外使用个别字段。非常快*代表页面将转到第四部分1.1阻止(Blocking)当一个线程通过上面所列的方式处于等待或暂停的状态,被称为被阻止。一旦被阻止,线程立刻放弃它被分配的CPU时间,将它的ThreadState属性添加为WaitSleepJoin状态,不在安排时间直到停止阻止。停止阻止在任意四种情况下发生(关掉电脑的电源可不算!):阻止的条件已得到满足操作超时(如果timeout被指定了)通过Thread.Interrupt中断了通过Thread.Abort放弃了当线程通过(不建议)Suspend方法暂停,不认为是被阻止了。1.2休眠和轮询调用Thread.Sleep阻止当前的线程指定的时间(或者直到中断):123456staticvoidMain(){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环境中用于单元模式。这在WindowsForms程序中是一个很大的问题,任何对主UI线程的阻止都将使程序失去相应。因此一般避免这样使用,无论信息汲取是否被“技术地”暂定与否。由COM遗留下来的宿主环境更为复杂,在一些时候它决定停止,而却保持信息的汲取存活。微软的ChrisBrumm在他的博客中讨论这个问题。(搜索:'COMChrisBrumme')线程类同时也提供了一个SpinWait方法,它使用轮询CPU而非放弃CPU时间的方式,保持给定的迭代次数进行“无用地繁忙”。50迭代可能等同于停顿大约一微秒,虽然这将取决于CPU的速度和负载。从技术上讲,SpinWait并不是一个阻止的方法:一个处于spin-waiting的线程的ThreadState不是WaitSleepJoin状态,并且也不会被其它的线程过早的中断(Interrupt)。SpinWait很少被使用,它的作用是等待一个在极短时间(可能小于一微秒)内可准备好的可预期的资源,而不用调用Sleep方法阻止线程而浪费CPU时间。不过,这种技术的优势只有在多处理器计算机:对单一处理器的电脑,直到轮询的线程结束了它的时间片之前,一个资源没有机会改变状态,这有违它的初衷。并且调用SpinWait经常会花费较长的时间这本身就浪费了CPU时间。1.3阻止vs.轮询线程可以等待某个确定的条件来明确轮询使用一个轮询的方式,比如:1while(!proceed);或者:1while(DateTime.NownextStartTime);这是非常浪费CPU时间的:对于CLR和操作系统而言,线程进行了一个重要的计算,所以分配了相应的资源!在这种状态下的轮询线程不算是阻止,不像一个线程等待一个EventWaitHandle(一般使用这样的信号任务来构建)。阻止和轮询组合使用可以产生一些变换:1while(!proceed)Thread.Sleep(x);//轮询休眠!x越大,CPU效率越高,折中方案是增大潜伏时间,任何20ms的花费是微不足道的,除非循环中的条件是极其复杂的。除了稍有延迟,这种轮询和休眠的方式可以结合的非常好。(但有并发问题,在第四部分讨论)可能它最大的用处在于程序员可以放弃使用复杂的信号结构来工作了。1.4使用Join等待一个线程完成你可以通过Join方法阻止线程直到另一个线程结束:123456789101112131415classJoinDemo{staticvoidMain(){Threadt=newThread(delegate(){Console.ReadLine();});t.Start();t.Join();//等待直到线程完成Console.WriteLine(Threadt'sReadLinecomplete!);}}Join方法也接收一个使用毫秒或用TimeSpan类的超时参数,当Join超时是返回false,如果线程已终止,则返回true。Join所带的超时参数非常像Sleep方法,实际上下面两行代码几乎差不多:123Thread.Sleep(1000);Thread.CurrentThread.Join(1000);(他们的区别明显在于单线程的应用程序域与COM互操作性,源于先前描述Windows信息汲取部分:在阻止时,Join保持信息汲取,Sleep暂停信息汲取。)2.锁和线程安全锁实现互斥的访问,被用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:1234567classThreadUnsafe{staticintval1,val2;staticvoidGo(){if(val2!=0)Console.WriteLine(val1/val2);val2=0;}8}这不是线程安全的:如果Go方法被两个线程同时调用,可能会得到在某个线程中除数为零的错误,因为val2可能被一个线程设置为零,而另一个线程刚好执行到if和Console.WriteLine语句。下面用lock来修正这个问题:123456789101112classThreadSafe{staticobjectlocker=newobject();staticintval1,val2;staticvoidGo(){lock(locker){if(val2!=0)Console.WriteLine(val1/val2);val2=0;}}}在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。互斥锁有时被称之对由锁所保护的内容强迫串行化访问,因为一个线程的访问不能与另一个重叠。在这个例子中,我们保护了Go方法的逻辑,以及val1和val2字段的逻辑。一个等候竞争锁的线程被阻止将在ThreadState上为WaitSleepJoin状态。稍后我们将讨论一个线程通过另一个线程调用Interrupt或Abort方法来强制地被释放。这是一个相当高效率的技术可以被用于结束工作线程。C#的lock语句实际上是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法:12345678Monitor.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也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。2.1选择同步对象任何对所有有关系的线程都可见的对象都可以作为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。服从这些规则,同步对象可以兼对象和保护两种作用。比如下面List:1234567891011classThreadSafe{Liststringlist=newListstring();voidTest(){lock(list){list.Add(Item1);...一个专门字段是常用的(如在先前的例子中的locker),因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:lock(this){...}或:lock(typeof(Widget)){...}//保护访问静态是不好的,因为这潜在的可以在公共范围访问这些对象。锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x)而被阻止,两者都要调用ock(x)来完成阻止工作。2.2嵌套锁定线程可以重复锁定相同的对象,可以通过多次调用Monitor.Enter或lock语句来实现。当对应编号的Monitor.Exit被调用或最外面的lock语句完成后,对象那一刻被解锁。这就允许最简单的语法实现一个方法的锁调用另一个锁:123456staticobjectx=newobject();staticvoidMain(){lock(x){Console.WriteLine(Ihavethelock);Nest();Console.WriteLine(Istillhavethelock);789101112131415}在这锁被释放}staticvoidNest(){lock(x){...}释放了锁?没有完全释放!}线程只能在最开始的锁或最外面的锁时被阻止。2.3何时进行锁定作为一项基本规则,任何和多线程有关的会进行读和写的字段应当加锁。甚至是极平常的事情——单一字段的赋值操作,都必须考虑到同步问题。在下面的例子中Increment和Assign都不是线程安全的:12345classThreadUnsafe{staticintx;staticvoidIncrement(){x++;}staticvoidAssign(){x=123;}}下面是Increment和Assign线程安全的版本:1234567classThreadUnsafe{staticobjectlocker=newobject();staticintx;staticvoidIncrement(){lock(locker)x++;}staticvoidAssign(){lock(locker)x=123;}}作为锁定另一个选择,在一些简单的情况下,你可以使用非阻止同步,在第四部分讨论(即使像这样的语句需要同步的原因)。2.4锁和原子操作如果有很多变量在一些锁中总是进行读和写的操作,那么你可以称之为原子操作。我们假设x和y不停地读和赋值,他们在锁内通过locker锁定:lock(locker){if(x!=0)y/=x;}你可以认为x和y通过原子的方式访问,因为代码段没有被其它的线程分开或抢占,别的线程改变x和y是无效的输出,你永远不会得到除数为零的错误,保证了x和y总是被相同的排他锁访问。2.5性能考量锁定本身是非常快的,一个锁在没有堵塞的情况下一般只需几十纳秒(十亿分之一秒)。如果发生堵塞,任务切换带来的开销接近于数微秒(百万分之

1 / 25
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功