C#的并发机制优秀在哪?( 二 )


这里我们先来科普一下CPU之间的通信MESI协议的内容 。我们知道现代的CPU都配备了高速缓存,按照多核高速缓存同步的MESI协议约定,每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid),其中:
M:代表该缓存行中的内容被修改,并且该缓存行只被缓存在该CPU中 。这个状态代表缓存行的数据和内存中的数据不同 。
E:代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容 。这个状态的缓存行中的数据与内存的数据一致 。
I:代表该缓存行中的内容无效 。
S:该状态意味着数据不止存在本地CPU缓存中,还存在其它CPU的缓存中 。这个状态的数据和内存中的数据也是一致的 。不过只要有CPU修改该缓存行都会使该行状态变成 I。
四种状态的状态转移图如下:

C#的并发机制优秀在哪?

文章插图
我们上文也提到了,不同的线程是有大概率是运行在不同CPU核上的,在不同CPU操作同一块内存时,站在CPU0的角度上看,就是CPU1会不断发起remote write的操作,这会使该高速缓存的状态总是会在S和I之间进行状态迁移,而一旦状态变为I将耗费比较多的时间进行状态同步 。
C#的并发机制优秀在哪?

文章插图
因此我们可以基本得出 this.Invoke(new DeleteDelegate(DeleteFileHandler), new object[] { sender,e }); ;这行看似无关紧要的代码之后,无意中使files共享变量的维护操作,由多核多线程共同操作,变成了众多子线程向主线程通信,所有维护操作均由主线程进行,这也使最终的执行效率有所提高 。
深度解读,为何要加两把锁
在当前使用通信替代共享内存的大潮之下,锁其实是最重要的设计 。
我们看到在.Net的Invoke实现中,使用了两把锁lock (this) 与lock (threadCallbackList) 。
lock( this) { if(threadCallbackList == null) { threadCallbackList = newQueue; } } lock(threadCallbackList) { if(threadCallbackMessage == 0) { threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); } threadCallbackList.Enqueue(tme); }在.NET当中lock关键字的基本可以理解为提供了一个近似于CAS的锁(Compare And Swap) 。CAS的原理不断地把"期望值"和"实际值"进行比较,当它们相等时,说明持有锁的CPU已经释放了该锁,那么试图获取这把锁的CPU就会尝试将"new"的值(0)写入"p"(交换),以表明自己成为spinlock新的owner 。伪代码演示如下:
voidCAS( intp, intold, intnew) { if*p != old donothing else *p ← new }基于CAS的锁效率没问题,尤其是在没有多核竞争的情况CAS表现得尤其优秀,但CAS最大的问题就是不公平,因为如果有多个CPU同时在申请一把锁,那么刚刚释放锁的CPU极可能在下一轮的竞争中获取优势,再次获得这把锁,这样的结果就是一个CPU忙死,而其它CPU却很闲,我们很多时候诟病多核SOC“一核有难,八核围观”其实很多时候都是由这种不公平造成的 。
为了解决CAS的不公平问题,业界大神们又引入了TAS(Test And Set Lock)机制,个人感觉还是把TAS中的T理解为Ticket更好记一些,TAS方案中维护了一个请求该锁的头尾索引值,由"head"和"tail"两个索引组成 。
structlockStruct{ int32head; int32tail; };"head"代表请求队列的头部,"tail"代表请求队列的尾部,其初始值都为0 。
最一开始时,第一个申请的CPU发现该队列的tail值是0,那么这个CPU会直接获取这把锁,并会把tail值更新为1,并在释放该锁时将head值更新为1 。
在一般情况下当锁被持有的CPU释放时,该队列的head值会被加1,当其他CPU在试图获取这个锁时,锁的tail值获取到,然后把这个tail值加1,并存储在自己专属的寄存器当中,然后再把更新后的tail值更新到队列的tail当中 。接下来就是不断地循环比较,判断该锁当前的"head"值,是否和自己存储在寄存器中的"tail"值相等,相等时则代表成功获得该锁 。
TAS这类似于用户到政务大厅去办事时,首先要在叫号机取号,当工作人员广播叫到的号码与你手中的号码一致时,你就获取了办事柜台的所有权 。
但是TAS却存在一定的效率问题,根据我们上文介绍的MESI协议,这个lock的头尾索引其实是在各个CPU之间共享的,因此tail和head频繁更新,还是会引发调整缓存不停的invalidate,这会极大的影响效率 。


推荐阅读