您好,欢迎访问三七文档
C#线程同步(1)-临界区&Lock预备知识:线程的相关概念和知识,有多线程编码的初步经验。临界区(CriticalSection)是一段在同一时候只被一个线程进入/执行的代码。为啥要有这个东西?是因为这段代码访问了“临界资源”,而这种资源只能同时被互斥地访问。举个例子来说,你的银行账户就是一个互斥资源,一个银行系统里面改变余额(存取)的操作代码就必须用在临界区内。如果你的账户余额是$100,000,假设有两个人同时给你汇款$50,000。有两个线程分别执行这两笔汇款业务,线程A在获取了你的账户余额后,在它把新余额($150000)储存回数据库以前,操作系统把这个线程暂停转而把CPU的时间片分给另一个线程(是的,这太巧了);那么线程B此时取出的账户余额仍然是$10000,随后线程B幸运的得到的CPU时间把$50000存入你的账户,那么余额变成$150000。而此后某个时候,线程A再次得以执行,它也把“新”余额$150000更新到系统……于是你的$50000就这么凭空消失了。是因为OS的多任务调度,其实在原因一里面已经提到。如果OS不支持多任务调度,那么线程A/线程B执行更新余额的操作总是一个接一个进行,那么完全不会有上面的问题了。在多线程的世界里,你必须随时做好你的代码执行过程随时失去控制的准备;你需要好好考虑当代码重新执行的时候,是否可以继续正确的执行。一句话,你的程序段在多线程的世界里,你所写的方法并不是“原子性”的操作。Lock关键字C#提供lock关键字实现临界区,MSDN里给出的用法:ObjectthisLock=newObject();lock(thisLock){//Criticalcodesection}lock实现临界区是通过“对象锁”的方式,注意是“对象”,所以你只能锁定一个引用类型而不能锁定一个值类型。第一个执行该代码的线程,成功获取对这个对象的锁定,进而进入临界区执行代码。而其它线程在进入临界区前也会请求该锁,如果此时第一个线程没有退出临界区,对该对象的锁定并没有解除,那么当前线程会被阻塞,等待对象被释放。既然如此,在使用lock时,要注意不同线程是否使用同一个“锁”作为lock的对象。现在回头来看MSDN的这段代码似乎很容易让人误解,容易让人联想到这段代码是在某个方法中存在,以为thisLock是一个局部变量,而局部变量的生命周期是在这个方法内部,所以当不同线程调用这个方法的时候,他们分别请求了不同的局部变量作为锁,那么他们都可以分别进入临界区执行代码。因此在MSDN随后真正的示例中,thisLock实际上是一个private的类成员变量:usingSystem;usingSystem.Threading;classAccount{privateObjectthisLock=newObject();intbalance;Randomr=newRandom();publicAccount(intinitial){balance=initial;}intWithdraw(intamount){//Thisconditionwillneverbetrueunlessthelockstatement//iscommentedout:if(balance0){thrownewException(NegativeBalance);}//Commentoutthenextlinetoseetheeffectofleavingout//thelockkeyword:lock(thisLock){if(balance=amount){Console.WriteLine(BalancebeforeWithdrawal:+balance);Console.WriteLine(AmounttoWithdraw:-+amount);balance=balance-amount;Console.WriteLine(BalanceafterWithdrawal:+balance);returnamount;}else{return0;//transactionrejected}}}publicvoidDoTransactions(){for(inti=0;i100;i++){Withdraw(r.Next(1,100));}}}classTest{staticvoidMain(){Thread[]threads=newThread[10];Accountacc=newAccount(1000);for(inti=0;i10;i++){Threadt=newThread(newThreadStart(acc.DoTransactions));threads[i]=t;}for(inti=0;i10;i++){threads[i].Start();}}}这个例子中,Account对象只有一个,所以临界区所请求的“锁”是唯一的,因此用类的成员变量是可以实现互斥意图的,其实用大家通常喜欢的lock(this)也未尝不可,也即请求这个Account实例本身作为锁。但是如果在某种情况你的类实例并不唯一或者一个类的几个方法之间都必须要互斥,那么就要小心了。必须牢记一点,所有因为同一互斥资源而需要互斥的操作,必须请求“同一把锁”才有效。假设这个Account类并不只有一个Withdraw方法修改balance,而是用Withdraw()来特定执行取款操作,另有一个Deposit()方法专门执行存款操作。很显然这两个方法必须是互斥执行的,所以这两个方法中所用到的锁也必须一致;不能一个用thisLock,另一个重新用一个privateObjectthisLock1=newObject()。再进一步,其实这个操作场景下各个互斥区存在的目的是因为有“Balance”这个互斥资源,所有有关Balance的地方应该都是互斥的(如果你不介意读取操作读到的是脏数据的话,当然也可以不用)。题外话:这么看来其实用Balance本身作为锁也许更为符合“逻辑”,lock住需要互斥的资源本身不是更好理解么?不过这里Balance是一个值类型,你并不能直接对它lock(你可能需要用到volatile关键字,它能在单CPU的情况下确保只有一个线程修改一个变量)。Lock使用的建议关于使用Lock微软给出的一些建议。你能够在MSDN上找到这么一段话:通常,应避免锁定public类型,否则实例将超出代码的控制范围。常见的结构lock(this)、lock(typeof(MyType))和lock(myLock)违反此准则:1.如果实例可以被公共访问,将出现lock(this)问题。2.如果MyType可以被公共访问,将出现lock(typeof(MyType))问题。3.由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现lock(myLock)问题。4.最佳做法是定义private对象来锁定,或privatestatic对象变量来保护所有实例所共有的数据。lock(this)的问题我是这么理解:处于某种原因Account在整个程序空间内不是唯一,那么不同Account实例的相应方法就不可能互斥,因为他们请求的是不同Accout实例内部的不同的锁。这时候微软示例中的privateObjectthisLock仍然也避免不了这个问题,而需要使用privatestaticObjectthisLock来解决问题,因为static变量是所有类实例共享的。猜想就算Account只有一个实例,但是如果在程序内部被多个处理不同任务的线程访问,那么Account实例可能会被某段代码直接作为锁锁定;这相当于你自己锁定了自己,而别人在不告诉你的情况下也可以能锁定你。这些情况都是你在写Account这个类的时候并没有办法作出预测的,所以你的Withdraw代码可能被挂起,在多线程的复杂情况下也容易造成死锁。不管怎样,你写这段代码的时候肯定不会期待外部的代码跟你使用了同一把锁吧?这样很危险。另外,从面向对象来说,这等于把方法内部的东西隐式的暴露出去。为了实现互斥,专门建立不依赖系this的代码机制总是好的;thisLock,专事专用,是个好习惯。MyType的问题跟lock(this)差不多理解,不过比lock(this)更严重。因为Lock(typeof(MyType))锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类对象(就是拥有Static成员的那个对象实例),锁定它就锁定了该对象的所有实例。同时lock(typeof(MyType))是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们都有可能锁定类对象,完全阻止你代码的执行,导致你自己代码的挂起或者死锁。至于lock(myLock),是因为在.NET中字符串会被暂时存放。如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用lock(“mylock”)的话,它们实际锁住的是同一个对象。.NET集合类对lock的支持在多线程环境中,常会碰到的互斥资源应该就是一些容器/集合。因此.NET在一些集合类中(比如ArrayList,HashTable,Queue,Stack,包括新增的支持泛型的List)已经提供了一个供lock使用的对象SyncRoot。在.Net1.1中大多数集合类的SyncRoot属性只有一行代码:returnthis,这样和lock(集合的当前实例)是一样的。不过ArrayList中的SyncRoot有所不同(这个并不是我反编译的,我并没有验证这个说法):get{if(this._syncRoot==null){Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);}returnthis._syncRoot;}题外话:上面反编译的ArrayList的代码,引出了个Interlocked类,即互锁操作,用以对某个内存位置执行的简单原子操作。举例来说在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:1、将实例变量中的值加载到寄存器中。2、增加或减少该值。3、在实例变量中存储该值。线程可能会在执行完前两个步骤后被夺走CPU时间,然后由另一个线程执行所有三个步骤。当第一个线程重新再开始执行时,它改写实例变量中的值,造成第二个线程执行增减操作的结果丢失。这根我们上面提到的银行账户余额的例子是一个道理,不过是更微观上的体现。我们使用该类提供了的Increment和Decrement方法就可以避免这个问题。另外,Interlocked类上提供了其它一些能保证对相关变量的操作是原子性的方法。如Exchange()可以保证指定变量的值交换操作的原子性,Read()保证在32位操作系统中对64位变量的原子读取。而这里使用的CompareExchange方法组合了两个操作:保证了比较和交换操作按原子操作执行。此例中CompareExchange方法将当前syncRoot和null做比较,如果相等,就用newobject()替换SyncRoot。在现代处理器中,Interlocked类的方法经常可以由单个指令来实现,因此它们的执行性能非常高。虽然Interlocked没有直接提供锁定或者发送信号的能力,但是你可以用它编写锁和信号,从而编写出高效的非阻止并发的应用程序。但是这需要复杂的低级别编程能力,因此大多数情况下使用lock或其它简单锁是更好的选择。看到这里是不是已经想给微软一耳光了?一边教导大家不要用lock(this),一边竟然在基础类库中大量使用……呵呵,我只能说据传从.Net2.0开始SyncRoot已经是会返回一个单独的类了,想来大约应该跟ArrayList那种实现差不多,有兴趣的可以反编译验证下。这里想说,代码是自己的写的,最好减少自己代码对外部环境的依赖,事实证明即便是.Net基础库也不是那么可靠。自己
本文标题:C#线程同步
链接地址:https://www.777doc.com/doc-4303296 .html