多线程并发同步总结
前段做Zookeeper的Client4Net,涉及多线程较多。完了对多线程同步做下总结。由于是简单的汇总,主要给自己复习用,代码就不贴出来了。
1.锁
Lock:
这个大家都知道没什么可以说的,内部使用Monitor实现,等同于try { Monitor.Enter(obj,ref lockTaKen); }finally { Monitor.Exit(obj); } 。
注意几点就好:
Lock(this)锁定当前对象,不建议;
Lock(type)锁定某类型,这个范围太大,不小心就是死锁;效率低,不建议;
Lock(string)这个比较疯狂,鉴于string的特殊性,所有Equal的string都会被锁住;没特殊要求不要使用;
Lock(obj): 推荐,声明 static volatile object作为LockToken;
Monitor:
静态类,作用范围全局。需要显式的Enter/TryEnter和Exist。比较简单不要将值类型当作参数传入就好,否则装箱后就变成锁定不同的对象了。
需要注意一点是Pulse/TryPulse和Wait相关的就绪(ready)队列和等待(waiting)队列,关于这个MSDN解释的比较模糊(就绪队列:包含准备获取锁的线程;等待队列:包含等待对象状态更改通知的线程)。
首先只有持有锁的线程才能调用Wait和Pulse,一旦Wait必须Pulse才能被唤醒,Wait后释放锁,并进入等待队列。最大的好处是Wait的线程一旦收到Pulse信号就会从等待队列中插队到就绪队列以使其能尽可能早的获取锁。最好使用其包含超时参数的重载,这样超时后会马上进入就绪队列并能根据其返回值决定是否再次Wait,也避免后续获取锁的线程在Pluse前Exit造成无限等待。
Mutex:
继承自WaitHandle, 静态方法全局可跨进程,实例化方法作用与当前域;
主要API为继承自WaitHandle的WaitOne: 等待waitHandle信号。堵塞当前线程,超过预期等待时间后返回False。信号等同MenuResetHandle、AutoResetHandle可用于互操作。这个用起来太危险了,跨进程锁定没有特殊理由建议换个方式实现进程同步。碰到过一次,Trouble Shooting过程生不如死。不多说。最后重现将内存全转储到文件一点点找才发现。
SpinLock: 自旋锁,这货是值类型,意味这更轻量级、更高效、大量使用也会更耗资源。(注意区分SpinWait)等待时通过自旋不退出CPU执行,不用切换效率高,粒度较细,如锁定时间较长将消耗CPU,非特殊原因不推荐使用。建议其他锁类型存在性能问题后再来试试这个。
ReadWriteLock/ReadWriteLockSlim: 读写锁,4.5推荐后者。读锁,写锁,可升级读锁;我写别人都不能读,任何人读时都不能写。类似与数据库的可重复读隔离级别。
Semaphore: 继承者WaitHandle,信号量,创建指定资源数的锁,可用以实现类似池的结构。初始化时指定最大资源数,资源数为零是堵塞新的资源申请线程,等待占用资源线程推出占用。和CountDownEvent相反。主要API为WaitOne 继承自WaitHandle,进入,不必多说。注意这个继承自WaitHandle,类本身没有继承自WaitHandle,没看源码,应该内嵌WaitHandle实例。所以依然放Lock没有放到通知里。Release: 退出,-1/-N。
2.通知
WaitHandle
应该算同步信号量最重要的一个基类(抽象类),提供Wait虚方法接口供子类实现了。同时提供WaitAll,WaitAny,SignalAndWait的静态方法用以调度协调跨子类的信号量。
Join : Thread方法,阻塞当前线程知道指定时间或某个线程终止。不用多说。
EventWaitHandle : 另一个重量级的类,继承自WaitHandle,通过设置EventHandleMode枚举设置手动或自动释放, 更常使用子类AutoResetEvent,ManualResetEvent。
主要API:
SignalAndWait: 静态,根据属性EventHandleMode设置信号类型,AutoRest: 调用一次发出一个释放信号,使一个调用该类实例的Set方法阻塞的线程继续,然后信号重置。
MenuRest:信号不会自动重置,调用后所有调用该类实例的Set方法阻塞的线程继续。
Set : 将事件状态设置为终止状态,允许一个或多个等待线程继续。某线程:“我暂停了哈,可以走了通知我”。
Reset : 将事件状态设置为非终止状态,导致线程阻止。handle广播:“你们可以走了”。(AutoRest:“走一个”;MenuRest:“都走吧”)。
WaitOne: 等待指定时间获取信号量,没等到返回False继续;指定时间内收到信号继续。类似与:if(ewh.WaitOne(1000)){//超时前获得信号}else{//超时没获得信号}。
AutoRestEvent/AutoRestEventSlim: 继承自EventWaitHandle,EventHandleMode = AutoRest的EventHandleMode。后者更轻量级,推荐后者。
MenualRestEvent/MenualRestEventSlim: 继承自EventWaitHandle,EventHandleMode = MenualRestEvent的EventHandleMode。后者更轻量级,推荐后者。
Barrier: 屏障,可是线程分阶段执行,配合Parallel使用可以完成这用的功能:第一阶段:3个独立任务,都完成进入下已阶段;下一阶段可以是1个或多个任务并行。构造函数可为一个Int和一个Action的参数,当SinalandWait的次数达到Int参数值时执行Action。Like this:
Barrier br = new(2,(b)=>{//do something;}); Action a = ()=>{//do 1; br.SinalandWait(); //do 2; br.SinalandWait(); //do 3}; Action b = ()=>{//do 1; br.SinalandWait(); //do 2; br.SinalandWait(); //do 3};
Action c = ()=>{//do 1; br.SinalandWait(); //do 2; br.SinalandWait(); //do 3};
Action d = ()=>{//do 1; br.SinalandWait(); //do 2; br.SinalandWait(); //do 3};
Parallel.Invoke(a,b,c); //Parallel.Invoke(a,b,c,d)//will throw exception;
主要API:
AddParticipant/AddParticipants: 添加一个/多个参与者(任务执行者,Action),这个接口怪怪的。不理解参数都是Int为啥设计成这样。
RemoveParticipant/RemoveParticipants :移除一个/多个参与者。
SinalAndWait: 线程告诉Barrier:“我到了啊,等在等着呢,什么时候继续喊我,我还有其他事要接着干呢”,Barrier:“好,都到齐了我会喊你继续的。”。
SpinWait: 轻量级信号量,通过自旋等待信号,不同退出CPU执行。
主要API:
Reset: 重载自旋计数器,重新开始计数。
Spinone: 自旋一次,通常在循环里使用,自旋一次后检查条件是否满足。
Count:已自旋次数。
SpinUnit:最重要函数,包含一个Func参数和一个时间,自旋一次后执行返回Func结果。返回False继续自旋,直到超时推出。
3. 连锁
InterLocked:原子操作类。提供加减、替换、递增、递减等原子操作。比较简单没什么可说的。