预备知识:线程的相关概念和知识,有多线程编码的初步经验。 一个机会,索性把线程同步的问题在C#里面的东西都粗略看了下。 第一印象,C#关于线程同步的东西好多,保持了C#一贯的大杂烩和四不象风格(Java/Delphi)。临界区跟Java差不多只不过关键字用lock替代了synchronized,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,另外又搞出来几个Event……让人甚是不明了。不管那么多,一个一个来吧。 临界区(Critical Section) 是一段在同一时候只被一个线程进入/执行的代码。为啥要有这个东西?
Lock关键字 C#提供lock关键字实现临界区,MSDN里给出的用法: Object thisLock = new Object(); lock实现临界区是通过“对象锁”的方式,注意是“对象”,所以你只能锁定一个引用类型而不能锁定一个值类型。第一个执行该代码的线程,成功获取对这个对象的锁定,进而进入临界区执行代码。而其它线程在进入临界区前也会请求该锁,如果此时第一个线程没有退出临界区,对该对象的锁定并没有解除,那么当前线程会被阻塞,等待对象被释放。 既然如此,在使用lock时,要注意不同线程是否使用同一个“锁”作为lock的对象。现在回头来看MSDN的这段代码似乎很容易让人误解,容易让人联想到这段代码是在某个方法中存在,以为thisLock是一个局部变量,而局部变量的生命周期是在这个方法内部,所以当不同线程调用这个方法的时候,他们分别请求了不同的局部变量作为锁,那么他们都可以分别进入临界区执行代码。因此在MSDN随后真正的示例中,thisLock实际上是一个private的类成员变量: using System; class Account class Test 这个例子中,Account对象只有一个,所以临界区所请求的“锁”是唯一的,因此用类的成员变量是可以实现互斥意图的,其实用大家通常喜欢的lock(this)也未尝不可,也即请求这个Account实例本身作为锁。但是如果在某种情况你的类实例并不唯一或者一个类的几个方法之间都必须要互斥,那么就要小心了。必须牢记一点,所有因为同一互斥资源而需要互斥的操作,必须请求“同一把锁”才有效。 假设这个Account类并不只有一个Withdraw方法修改balance,而是用Withdraw()来特定执行取款操作,另有一个Deposit()方法专门执行存款操作。很显然这两个方法必须是互斥执行的,所以这两个方法中所用到的锁也必须一致;不能一个用thisLock,另一个重新用一个private Object thisLock1 = new Object()。再进一步,其实这个操作场景下各个互斥区存在的目的是因为有“Balance”这个互斥资源,所有有关Balance的地方应该都是互斥的(如果你不介意读取操作读到的是脏数据的话,当然也可以不用)。 题外话: Lock使用的建议 关于使用Lock微软给出的一些建议。你能够在MSDN上找到这么一段话: 通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则: lock(this)的问题我是这么理解:
至于lock("myLock"),是因为在.NET中字符串会被暂时存放。如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。 .NET集合类对lock的支持 在多线程环境中,常会碰到的互斥资源应该就是一些容器/集合。因此.NET在一些集合类中(比如ArrayList,HashTable,Queue,Stack,包括新增的支持泛型的List)已经提供了一个供lock使用的对象SyncRoot。 在.Net1.1中大多数集合类的SyncRoot属性只有一行代码:return this,这样和lock(集合的当前实例)是一样的。不过ArrayList中的SyncRoot有所不同(这个并不是我反编译的,我并没有验证这个说法): get 题外话:
另外,Interlocked类上提供了其它一些能保证对相关变量的操作是原子性的方法。如Exchange()可以保证指定变量的值交换操作的原子性,Read()保证在32位操作系统中对64位变量的原子读取。而这里使用的CompareExchange方法组合了两个操作:保证了比较和交换操作按原子操作执行。此例中CompareExchange方法将当前syncRoot和null做比较,如果相等,就用new object()替换SyncRoot。 在现代处理器中,Interlocked 类的方法经常可以由单个指令来实现,因此它们的执行性能非常高。虽然Interlocked没有直接提供锁定或者发送信号的能力,但是你可以用它编写锁和信号,从而编写出高效的非阻止并发的应用程序。但是这需要复杂的低级别编程能力,因此大多数情况下使用lock或其它简单锁是更好的选择。 看到这里是不是已经想给微软一耳光了?一边教导大家不要用lock(this),一边竟然在基础类库中大量使用……呵呵,我只能说据传从.Net2.0开始SyncRoot已经是会返回一个单独的类了,想来大约应该跟ArrayList那种实现差不多,有兴趣的可以反编译验证下。 这里想说,代码是自己的写的,最好减少自己代码对外部环境的依赖,事实证明即便是.Net基础库也不是那么可靠。自己能想到的问题,最好自己写代码去处理,需要锁就自己声明一个锁;不再需要一个资源那么自己代码去Dispose掉(如果是实现IDisposable接口的)……不要想着什么东西系统已经帮你做了。你永远无法保证你的类将会在什么环境下被使用,你也无法预见到下一版的Framework是否偷偷改变了实现。当你代码莫名其妙不Work的时候,你是很难找出由这些问题引发的麻烦。只有你代码足够的独立(这里没有探讨代码耦合度的问题),才能保证它足够的健壮;别人代码的修改(哪怕是你看来“不当”的修改),造成你的Code无法工作不是总有些可笑么(我还想说“苍蝇不叮无缝的蛋”“不要因为别人的错误连累自己”)? 一些集合类中还有一个方法是和同步相关的:Synchronized,该方法返回一个集合的内部类,该类是线程安全的,因为他的大部分方法都用lock来进行了同步处理(你会不会想那么SyncRoot显得多余?别急。)。比如,Add方法会类似于: public override void Add(objectkey,objectvalue) 不过即便是这个Synchronized集合,在对它进行遍历时,仍然不是一个线程安全的过程。当你遍历它时,其他线程仍可以修改该它(Add、Remove),可能会导致诸如下标越界之类的异常;就算不出错,你也可能读到脏数据。若要在遍历过程中保证线程安全,还必须在整个遍历过程中锁定集合,我想这才是SynRoot存在的目的吧: Queue myCollection = newQueue(); 提供SynRoot是为了把这个已经“线程安全”的集合内部所使用的“锁”暴露给你,让你和它内部的操作使用同一把锁,这样才能保证在遍历过程互斥掉其它操作,保证你在遍历的同时没有可以修改。另一个可以替代的方法,是使用集合上提供的静态ReadOnly()方法,来返回一个只读的集合,并对它进行遍历,这个返回的只读集合是线程安全的。 到这里似乎关于集合同步的方法似乎已经比较清楚了,不过如果你是一个很迷信MS基础类库的人,那么这次恐怕又会失望了。微软决定所有从那些自Framwork 3.0以来加入的支持泛型的集合中,如List,取消掉创建同步包装器的能力,也就是它们不再有Synchronized,IsSynchronized也总会返回false;而ReadOnly这个静态方法也变为名为AsReadOnly的实例方法。作为替代,MS建议你仍然使用lock关键字来锁定整个集合。 至于List之类的泛型集合SyncRoot是怎样实现的,MSDN是这样描述的“在 List<(Of <(T>)>) 的默认实现中,此属性始终返回当前实例。”,赶紧去吐血吧! 自己的SyncRoot 还是上面提过的老话,靠自己,以不变应万变: public class MySynchronizedList 自已写一个类,用自己的syncRoot封装一个线程安全的容器。 |
|
来自: 昵称26960982 > 《C#》