多线程中的锁系统(四)-谈谈自旋锁
阅读目录:
基础
内核锁:基于内核对象构造的锁机制,就是通常说的内核构造模式。用户模式构造和内核模式构造
优点:cpu利用最大化。它发现资源被锁住,请求就排队等候。线程切换到别处干活,直到接受到可用信号,线程再切回来继续处理请求。
缺点:托管代码->用户模式代码->内核代码损耗、线程上下文切换损耗。
在锁的时间比较短时,系统频繁忙于休眠、切换,是个很大的性能损耗。
自旋锁:原子操作+自循环。通常说的用户构造模式。 线程不休眠,一直循环尝试对资源访问,直到可用。
优点:完美解决内核锁的缺点。
缺点:长时间一直循环会导致cpu的白白浪费,高并发竞争下、CPU的消耗特别严重。
混合锁:内核锁+自旋锁。 混合锁是先自旋锁一段时间或自旋多少次,再转成内核锁。
优点:内核锁和自旋锁的折中方案,利用前二者优点,避免出现极端情况(自旋时间过长,内核锁时间过短)。
缺点: 自旋多少时间、自旋多少次,这些策略很难把控。
在操作系统及net框架层,这块算法策略做的已经非常优了,有些API函数也提供了时间及次数可配置项,让使用者根据需求自行判断。
自旋锁示例
来看下我们自己简单实现的自旋锁:
int signal = 0; var li = new List<int>(); Parallel.For(0, 1000 * 10000, r => { while (Interlocked.Exchange(ref signal, 1) != 0)//加自旋锁 { //黑魔法 } li.Add(r); Interlocked.Exchange(ref signal, 0); //释放锁 }); Console.WriteLine(li.Count); //输出:10000000
上面就是自旋锁:Interlocked.Exchange+while
1:定义signal 0可用,1不可用。
2:Parallel模拟并发竞争,原子更改signal状态。 后续线程自旋访问signal,是否可用。
3:A线程使用完后,更改signal为0。 剩余线程竞争访问资源,B线程胜利后,更改signal为1,失败线程继续自旋,直到可用。
SpinLock
SpinLock是net4.0后Net提供的自旋锁类库,内部做了优化。
简单看下实例:
var li = new List<int>(); var sl = new SpinLock(); Parallel.For(0, 1000 * 10000, r => { bool gotLock = false; //释放成功 sl.Enter(ref gotLock); //进入锁 li.Add(r); if (gotLock) sl.Exit(); //释放 }); Console.WriteLine(li.Count); //输出:10000000
继续SpinLock
new SpinLock(false) 这个构造函数主要用来检查死锁用,true是开启。
在开启状态下,一旦发生死锁会直接抛异常的。
SpinLock实现的部分源码:
从代码中发现SpinLock并不是简单的实现那样一直自旋,其内部做了很多优化。
1:内部使用了Interlocked.CompareExchange保持原子操作, m_owner 0可用,1不可用。
2:第一次获得锁失败后,继续调用ContinueTryEnter,ContinueTryEnter有三种获得锁的情况。
3:ContinueTryEnter函数第一种获得锁的方式,使用了while+SpinWait。
4:第一种方式达到最大等待者数量后,命中走第二种。 继续自旋 turn * 100次。100这个值是处理器核数(4, 8 ,16)下最好的。
5:第二种如果还不能获得锁,走第三种。这种就带有混合构造的意思了,如下:
if (yieldsoFar % 40 == 0) Thread.Sleep(1); else if (yieldsoFar % 10 == 0) Thread.Sleep(0); else Thread.Yield();
Thread.Sleep(1) : 终止当前线程,放弃剩下时间片 休眠1毫秒, 退出跟其他线程抢占cpu。当然这个一般会更多,系统无法保证这么细的时间粒度。
Thread.Sleep(0): 终止当前线程,放弃剩下时间片。 但立马还会跟其他线程抢cpu,能不能抢到跟线程优先级有关。
Thread.Yeild(): 结束当前线程,让出CPU给其他准备好的线程。其他线程ok后或没有还没有准备好,继续执行当前,Thread.Yeild()会返回个bool值,表示CPU是否让出成功。
从源码中可以学到不少编程技巧,比如可以借鉴自旋+Thread.Yeild() 或 while+Thread.Yeild()等组合使用方式。
总结
本章介绍了自旋锁的基础及楼主的经验。 关于SpinLock类源码这块,只简单理解了下并没有深究。
测试了下SpinLock和自己实现的自旋锁性能对比(并行添加1000w List<int>()),SpinLock是单纯的自旋锁性能2倍以上。
另外测试了lock的性能,是系统SpinLock性能的3倍以上,可见lock内部自旋的效率更高,CLR暂没开源,看不到CLR具体实现的代码。
参考http://www.projky.com/dotnet/4.0/System/Threading/SpinLock.cs.html
如果您认为这篇文章还不错或有所收获,欢迎打赏因为你的支持是我继续写作,分享的动力!