混合模式
因为用户模式和内核模式各有优劣,为了利用两者各自的有点,因而能够同时应用两种模式来进行结构,在没有线程竞争的时候能够具备用户模式的性能劣势,而在多个线程同时竞争一个结构的时候又能提供不产生自旋的长处,使应用程序的性能失去晋升。
示例代码
class HybridLock : IDisposable{ private int m_Waiters = 0; private AutoResetEvent m_WaiterLock = new AutoResetEvent(false); public void Enter() { //线程想要取得锁 if (Interlocked.Increment(ref this.m_Waiters) == 1) return; //锁能够应用直,接返回 //另一个线程正在期待,阻塞该线程 this.m_WaiterLock.WaitOne(); //产生较大的性能影响 //WaitOne返回后,这个线程便领有了锁 } public void Leave() { //这个线程筹备开释锁 if (Interlocked.Decrement(ref this.m_Waiters) == 0) return; //没有其它线程阻塞,间接返回 //有其它线程正在阻塞,唤醒其中一个 this.m_WaiterLock.Set(); //产生较大的性能影响 } public void Dispose() { this.m_WaiterLock.Dispose(); }}
剖析代码
HybridLock对象的Enter办法调用了Interlocked.Increment,让m_Waiters字段递增1,这个线程发现没有线程领有这个锁,因而该线程取得锁并间接返回(取得锁的速度十分快)。如果另一个线程染指并调用Enter,此时m_Waiters字段再次递增值为2,发现锁被另一个线程领有,所以这个线程会调用AutoResetEvent对象的WaitOne来阻塞自身(这里因为是内核模式所以会产生性能影响),避免自旋。
再看看Leave办法,一个线程调用Leave时,会调用Interlocked.Decrement使m_Waiters字段递加1,如果m_Waiters字段是0,阐明没有其它线程在Enter的调用中产生阻塞,这时线程能够间接返回。然而如果线程发现m_Waiters字段的值递加后不为0,阐明存在线程竞争,至多有一个线程在内核中阻塞,这个线程必须只能唤醒一个阻塞的线程,通过调用AutoResetEvent对象的Set办法实现,
自旋、线程所有权和递归
因为转换成内核代码会造成性能损失,而线程占有一个锁的工夫通常比拟短,所以能够先让线程处于用户模式且自旋一段时间,若还未取得锁的权限便可让它转为内核模式,如果线程在期待期间锁变得可用便可防止转为内核模式了。
示例代码(提供自旋、线程所有权和递归反对)
class HybridLock : IDisposable{ //用户模式 private int m_Waiters = 0; //内核模式 private AutoResetEvent m_WaiterLock = new AutoResetEvent(false); //管制自旋 private int m_SpinCount = 1000; //用有锁的线程 private int m_OwningThreadId = 0; //领有次数 private int m_Recursion = 0; public void Enter() { //如果线程曾经领有,递增递归次数并返回 var threadId = Thread.CurrentThread.ManagedThreadId; if(this.m_OwningThreadId == threadId) { this.m_Recursion++; return; } //尝试获取 var spinWait = new SpinWait(); for (int i = 0; i < this.m_SpinCount; i++) { //如果锁能够应用了 if (Interlocked.CompareExchange(ref this.m_Waiters, 1, 0) == 0) goto GotLock; //给其它线程运行的机会,心愿锁会被开释 spinWait.SpinOnce(); } //自旋完结,依然没有取得锁则再试一次 if(Interlocked.Increment(ref this.m_Waiters) > 1) { //有其它线程被阻塞,这个线程也必须阻塞 this.m_WaiterLock.WaitOne(); //性能损失 //期待该线程用有锁醒来 } GotLock: //一个线程用有锁时,记录ID并指出线程领有锁一次 this.m_OwningThreadId = threadId; this.m_Recursion = 1; } public void Leave() { //如果线程不必有锁,bug var threadId = Thread.CurrentThread.ManagedThreadId; if (threadId != this.m_OwningThreadId) throw new SynchronizationLockException("线程未领有锁!"); //递加递归计数,如果线程依然用有锁,间接返回 if (--this.m_Recursion > 0) return; //当初没有线程领有锁 this.m_OwningThreadId = 0; //若没有其它线程被阻塞间接返回 if (Interlocked.Decrement(ref this.m_Waiters) == 0) return; //唤醒一个被阻塞的线程 this.m_WaiterLock.Set(); //性能损失 } public void Dispose() { this.m_WaiterLock.Dispose(); }}
Monitor类
这个类提供了一个互斥锁,这个锁反对自旋、线程所有权和递归,是线程同步罕用的类。
工作形式
CLR初始化时调配一个同步块数组,一个对象在堆中创立的时候,都有两个额定的字段与它关联,第一个字段是“类型对象指针”,它蕴含类型对象的内存地址。第二个字段是“同步索引块”,它蕴含同步块数组中的一个整数索引。
一个对象在结构时,对象的同步块索引初始化为-1,表明不援用任何同步块。而后调用Monitor.Enter时,CLR在数组中找到一个空白同步块,并设置对象的同步块索引来援用该同步块。调用Exit时,会查看是否有其它任何线程正在期待应用对象的同步块,如果没有线程在期待,同步块就能自在被应用了,Exit将对象的同步块索引改回-1,自在的同步块未来能够和另一个对象关联。
堆中对象的同步块索引和CLR的同步块数组元素之间的关系图
Monitor的应用
class SomeType{ //对象公有锁 private readonly object m_Lock = new object(); public void DoSomething() { //进入公有锁 Monitor.Enter(this.m_Lock); //其它代码... //退出公有锁 Monitor.Exit(this.m_Lock); }}
ReaderWriterLockSlim类
性能介绍
- 一个线程写入数据时,申请拜访的其它所有线程都被阻塞
- 一个线程读取数据时,申请读取的其它线程容许继续执行,但申请写入的线程还是会被阻塞
- 写入数据的线程完结后,能够解除一个写入线程的阻塞,使它能写入数据,也能够解除所有读取线程的阻塞,让它们并发读取数据。如果没有线程被阻塞,锁就会进入自在状态,容许下一个reader或writer线程获取
- 读取数据的所有线程完结后,一个writer线程会被解除阻塞,使它能写入数据,如果没有线程被阻塞,锁就会进入自在状态,容许下一个reader或writer线程获取
ReaderWriterLockSlim用法
class SomeType : IDisposable{ private readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); public void DoWrite() { this.m_Lock.EnterWriteLock(); //其它代码... //退出公有锁 this.m_Lock.ExitWriteLock(); } public void DoRead() { this.m_Lock.EnterReadLock(); //其它代码... this.m_Lock.ExitReadLock(); } public void Dispose() { this.m_Lock.Dispose(); }}
在结构ReaderWriterLockSlim对象的时候容许传递一个LockRecursionPolicy标记,如果传递LockRecursionPolicy.SupportsRecursion,锁就能够反对线程所有权和递归行为(这些行为会对锁的性能产生负面影响),如果反对这些行为锁必须跟踪容许进入锁的所有reader线程,同时为每个线程都独自保护一个递归技术,破费更大的代价,因而倡议结构的时候传入LockRecursionPolicy.NoRecursion。
双检锁(提早初始化)
class Singleton{ private static object s_Lock = new object(); private static Singleton s_Value = null; //公有结构 private Singleton() { } public static Singleton GetSingleton() { if (s_Value != null) return s_Value; //让一个线程创立它 Monitor.Enter(s_Lock); if (s_Value == null) { var temp = new Singleton(); Interlocked.Exchange(ref s_Value, temp); } Monitor.Exit(s_Lock); return s_Value; }}
为何应用Interlocked.Exchange
为何不间接s_Value=new Singleton(),起因在于这种写法编译器可能先为Singleton分配内存,再将援用赋值给s_Value,最初调用结构器。从单线程来看这样并没什么问题,如果是多线程,此时在援用曾经赋值给s_Value了,然而却还未调用结构器,这时另一个线程调用了GetSingleton办法,发现s_Value不为null,所以开始应用Singleton对象,然而对象的结构器还未执行完结,产生bug。而应用Interlocked.Exchange能够修改该问题,办法保障temp中的援用只有在结构器执行完结后才赋值给s_Value。
Monitor类的Wait与Pulse
如果一个线程心愿一个复合条件为true时执行一些代码,便能够应用Wait与Pulse,在条件不满足的时候Wait,另一个线程更改条件后Pulse,而非让线程自旋间断检测条件,节约CPU工夫。
示例代码
class ConditionVariablePattern{ private readonly object m_Lock = new object(); private bool m_Condition = false; public void Thread1() { //获取一个互斥锁 Monitor.Enter(this.m_Lock); //在锁中原子性地检测复合条件 while (!this.m_Condition) { //条件不满足,期待其它线程批改 Monitor.Wait(this.m_Lock); } //条件满足,解决数据... Monitor.Exit(this.m_Lock); } public void Thread2() { //获取一个互斥锁 Monitor.Enter(this.m_Lock); //解决数据并批改条件 this.m_Condition = true; //Monitor.Pulse(this.m_Lock); //开释之后唤醒一个正在期待的线程 Monitor.PulseAll(this.m_Lock); //开释之后唤醒所有正在期待的线程 Monitor.Exit(this.m_Lock); }}
代码解析
Thread1获取一个互斥锁,而后对一个条件变量进行检测,如果条件不满足,调用Monitor.Wait开释锁并阻塞调用线程,其它线程能取得改锁。
Thread2获取锁的所有权,解决数据,造成一些状态的扭转,其中包含Thread1要检测的条件变量,而后调用Monitor.PulseAll或者Monitor.Pulse,从而解除一个因为调用Wait办法而进入阻塞的线程。
其中Pulse只解除期待最久的线程的阻塞,PulseAll解除所有期待线程的阻塞,然而解除阻塞的线程还必须期待Thread2线程调用完Exit能力领有锁。
Thread1醒来时,进行下一次循环迭代,再次对条件进行检测,如过条件仍为false,持续调用Wait。如果条件满足,解决一些数据,最初调用Exit永恒开释锁。
BlockingCollection类实现生产者/消费者模式
static void Main(){ //BlockingCollection类实现生产者/消费者模式 var bl = new BlockingCollection<int>(new ConcurrentQueue<int>()); //由一个线程池执行生产 ThreadPool.QueueUserWorkItem(ConsumeItems, bl); //在汇合中增加数据项 for (int i = 0; i < 6; i++) { Console.WriteLine("production item : {0}", i); bl.Add(i); } //告诉生产线程不会在汇合中增加更多的item了 bl.CompleteAdding(); Console.ReadKey();}