北京医院白癜风治疗 https://baike.baidu.com/item/%E5%8C%97%E4%BA%AC%E4%B8%AD%E7%A7%91%E7%99%BD%E7%99%9C%E9%A3%8E%E5%8C%BB%E9%99%A2/9728824 提起多线程编程,始终离不开线程安全(资源竞争)的问题。如果没有处理好这些问题,往往在会出现开发一时爽,调试火葬场的情况。大都数语言中都会提供一些特定的方法来简化多线程开发,比如C#就提供了lock关键字来解决这些问题。 如果你在开发的过程中正确的使用了lock关键字,将有效的避免许多线程安全的问题。但是任何解决方案都是存在代价的,一味使用lock的话也会照成意想不到的性能(逼格)损失。本文就列举了5种情况下应避免使用lock关键字。 使用System.Collections.Concurrent命名空间比如直接使用ConcurrentDictionary替代Dictionary,而不是使用lock: //使用lock+Dictionarylock(dict){if(dict.ContainsKey(key)){dict[key]=dict[key]+value;}else{dict.Add(key,value);}}//使用ConcurrentDictionarydict.AddOrUpdate(key,value,(k,v)=v+value); 在需要线程安全的情景下应该使用ConcurrentDictionaryTKey,TValue代替DictionaryTKey,TValue,ConcurrentBagT代替ListT,同时还有线程安全的ConcurrentQueueT、ConcurrentStackT等在该命名空间可供使用。值得注意的是ConcurrentBag是一个无序的集合同时也并不实现IList接口,所以无法使用索引,也无法在需要IList的地方代替。 我相信大部分人都知道System.Collections.Concurrent这个命名空间,本不想写这一段,但是有点不可思议是我经常见因为不知道ConcurrentDictionary的存在而手写了一个“SynchronizedDictionary”的人,而且一般手写的“SynchronizedDictionary”都是有很大问题的,这点可以从ConcurrentDictionary选择显示实现IDictionary接口上学到不少设计一个线程安全对象API的技巧。 使用Interlocked使用Interlocked而不是使用lock: //使用locklock(counter){counter.Progress+=val;}//使用InterlockedInterlocked.Add(refcounter.Progress,val); Interlocked对于int,long这样的基础类型进行线程安全的运算操作时特别方便。线程安全要求保证对共享变量的任何写入或读取访问都是原子的,不然的话,你正在处理数据可能已不可用,或者读取出来的值可能不正确。总之,使用Interlocked不仅性能比lock更好,而且代码更为清晰简单。 使用ThreadStaticAttribute或ThreadLocal在处理一些特定场景中需要为每个线程提供单独的变量,比如于多线程 下载时,每个线程都需要记录下载的字节数,那么使用ThreadStaticAttribute最合适不过了: //使用ConcurrentDictionary下载字节数ConcurrentDictionaryint,intThreadDownloadBytes=newConcurrentDictionaryint,int();voidDownloadFileThread(){//...记录下载字节数ThreadDownloadBytes.AddOrUpdate(Thread.CurrentThread.ManagedThreadId,readedByteCount,(k,v)=v+readedByteCount);}//使用ThreadStaticAttribute来使不同线程有各自独立的存储空间[ThreadStatic]staticintThreadDownloadBytes;voidDownloadFileThread(){//...记录下载字节数ThreadDownloadBytes+=readedByteCount;} 上面的场景可能跟实际应用中有出入,只是作为例子说明问题。你可能会说这个场景中并没有使用lock啊,那是已经使用ConcurrentDictionary来简化代码。 但此处请注意,使用ThreadStaticAttribute特性标注的静态变量的初始化并不可靠,因为初始化这个行为只在一个线程发生。如果需要对对象进行初始化或者读取不同线程储存的值可以考虑使用ThreadLocalT。 使用ReaderWriterLockSlim在需要对资源读写的线程安全中,简单使用lock没有太多问题,但是如果这个资源的读取频率高,写入频率相对比较低,则可以使用ReaderWriterLockSlim来进一步提升性能,这种情况在一些需要线程安全的缓存场景特别常见。 //使用lock方法进行读写的线程安全管理privateobjectlockObject=newobject();privatevoidRead(){lock(lockObject){//具体实现}}privatevoidWrite(stringvalue){lock(lockObject){//具体实现}}//使用ReaderWriterLockSlim进行类似的操作privateReaderWriterLockSlimLockSlim=newReaderWriterLockSlim();privatevoidRead(){LockSlim.EnterReadLock();try{//具体实现}finally{LockSlim.ExitReadLock();}}privatevoidWrite(stringvalue){LockSlim.EnterWriteLock();try{//具体实现}finally{LockSlim.ExitWriteLock();}} 看起来好像ReaderWriterLockSlim更麻烦一点,不过之所以ReaderWriterLockSlim性能更好,是因为它可以允许多个线程进行读取操作,而当进行写入操作时进入独占模式。如果你的场景读取和写入频率不确定,则不应该使用ReaderWriterLockSlim。ReaderWriterLockSlim提供了许多方法对资源进行控制,请务必详读MSDN里相关内容后再尝试开始使用。 使用SpinLock自旋锁其实我不应该在这里提到SpinLock,因为如果读这篇文章到这里还没离开的人可能无法正确区分SpinLock的适用场景,虽然它的用法跟不加糖的lock特别相似。 //lockprivatestaticobjectlockObject=newobject();privatevoidUpdate(DateTimed){lock(lockObject){list.Add(d);}}//SpinLockprivatestaticSpinLockspinLock=newSpinLock();privatevoidUpdateWithSpinLock(DateTimed){boollockTaken=false;try{spinLock.Enter(reflockTaken);list.Add(d);}finally{if(lockTaken)spinLock.Exit(false);}} 简单的解释lock和SpinLock之间区别是,lock会在资源发生竞争的时候会切换去执行其它代码等待时机,类似于Thread.Sleep会把CPU时间让出去;而SpinLock在发生资源竞争时尝试自旋几个周期再去尝试,类似执行一个dowhile循环,消耗CPU时间。而且SpinLock是一个struct在大量使用的情况下对GC友好。所以当你确认锁独占资源的时间非常短,并且也没有使用其它你不知道源码的方法,可以考虑使用SpinLock来代替lock。 由于我的表达水平有限,实际情况远比上面的说法要复杂,所以关于SpinLock适用场景我直接摘抄MSDN的内容:[1] 如果共享资源上的锁不会保留太长时间,SpinLock可能会很有用。在这种情况下,多核计算机上的阻止线程可高效旋转几个周期,直到锁被释放。通过旋转,线程不会受到阻止,这是一个占用大量CPU资源的进程。但是MSDN上关于不适于使用SpinLock的情况更多:[2] 通常,在持有自旋锁时,应避免使用以下任何操作:1.堵塞2.调用自身可能会阻止的任何内容,3.同时保留多个自旋锁,4.进行动态调度的调用(interface和虚方法),5.对任何代码进行静态调度调用,而不是任何代码,或6.分配内存。最后多线程编程并不容易驾驭,不然也不会出现“一核有难多核围观”的梗,本文目的也只是抛砖引玉,本人水平有限也不想大篇幅的深入底层细节。上面每个话题在MSDN上都有大篇幅的内容可供深入阅读,毕竟微软的文档质量不是盖的。最后,写这么个不入流的文章只是希望看到有人大篇幅的使用lock关键字的时候,能有篇东西能发给TA看。 转载请注明原文网址:http://www.helimiaopu.com/bbqb/7779.html |