到底差在了什么地方:Cs->MUTEX->Monitor->WaitHandle

虽然我们整篇都在讨论.NET下的Multi-threading的问题,但是实际上很多问题都是可以类推的。例如前几次我们反复的说道了关于CriticalSection的问题。说它比MUTEX有何优越之处,例如速度就是一个明显的优势。但是从留言中发现在这个问题上存在着一些误会。今天不妨就闲扯一下这个问题。

 

首先说明一点,CriticalSection并不是100%生存在用户态下。因此说其效率比MUTEX“高”是要有前提条件的。实际上,我们说,如果CriticalSection可以非常顺利的一次性获得锁或者在一定的自旋之内获得锁,那么他肯定不必切换到内核状态。这种技巧就像是使用LockBit的自旋锁一样。但是如果在指定的非常短的时间内没有获得所而必须进入等待状态,就不得不指望内核对象。这时CriticalSection就会创建内核对象,可能是一个Event也可能是Semaphore。我正在下载Win2000的Src,不知道里面有没有东西会提到这个,等找到了再补上来。但是想一想的话也能够大概了解到,CriticalSection一定干了以下的事情:

 

(1)看看当前的线程是不是已经获得了这个锁。如果已经获得了这个锁则仅仅增加递归计数(Cs,Mutex均属递归锁)。跳过以下的步骤。
(2)试图锁住一个替代锁的变量。(就是刚才说的LockBit/Byte/Short/Long之类的东西),如果成功,则宣布该锁已经被这个线程获得了,否则就很遗憾了。
(3)如果用户采用了InitializeCriticalSectionAndSpinCount的函数初始化Cs,则试图自旋并检查这个替代的变量,如果成功则仍然可以避免接下来的操作。
(4)初始化内和对象(如果还没有)并调用WaitForXxx进行等待,这一步已经完全退化为一个内核对象了。(如果公司对规范之外的源代码注释不做要求估计这里有一些 /*Oh, god! Finally we have to wait, @#$%(Y */的注释)

 

这已经完全说明问题了,如果你保证几乎每一次对共享资源的独占操作都会很耗时,并且存在一定规模的竞争,那么实际上Cs的优势几乎荡然无存了(当然对于这种情况我们应该想想办法)。好的。下面我们推广一下。我们可以说,.NET Framework中的Monitor就类似于Cs(这可并不是说Monitor就是用Cs实现的,应当说它和Cs的策略相当),而从WaitHandle派生的Mutex,Semaphore等同步对象就与MUTEX的实现策略相当。我们没有.NET虚拟机的真正代码,但是从SSCLI与MONO的实现来看的确是如此的。

 

首先说说Mono,我使用的是Mono 1.9.1的源代码。话说在其中寻找相关的实现非常容易,首先在<install dir>\mcs\class\corelib\system.threading\monitor.cs中找到Monitor.Enter方法,这个方法调用了InternalCall的Monitor_try_enter方法。在虚拟机目录下狂搜Monitor_try_enter,找到了其实现文件:<install dir>\mono\metadata\monitor.c文件。核心部分在mono_monitor_try_enter_internal函数中。

 

这个函数太大了,就不粘在下面了,感兴趣的朋友可以自行阅读。大概意思是这个样的,首先,判断当前的Object有没有分配同步对象(不是内核对象啊),同步对象使用如下的数据结构表示:

 

struct _MonoThreadsSync
{
 gsize owner;   /* thread ID */
 guint32 nest;
#ifdef HAVE_MOVING_COLLECTOR
 gint32 hash_code;
#endif
 volatile gint32 entry_count;
 HANDLE entry_sem;
 GSList *wait_list;
 void *data;
};

 

我们发现了不少Cs的影子,首先owner可以作为递归检查的参数,并且可以作为dedicate variable。而真正在等待中发挥作用的则是HANDLE entry_sem,它在必要的时候使用CreateSemaphore(...)进行初始化。其余的我们暂时不关切,例如wait_list这个成员在Pulse和Wait中发挥很大作用。

 

接着上面的说,如果一个Object没有分配同步对象只能说明这个Object从来就没有被Lock过,于是获得一个全局的锁(方法是mono_monitor_allocator_lock(),它是一个宏,实际上就是EnterCriticalSection),分配一个同步对象,并且使用CAS(就是InterlockedCompareExchange)将其与该Object关联起来,如果成功,则成功的锁定了这个对象,退出(完全没有分配或者使用实内核同步对象)!如果失败了(这个操作可能失败,原因是从判断到获得锁的过程中可能其他的线程已经为这个对象分配了同步对象)则销毁分配的同步对象。

 

接下来与Cs做的事情大同小异,首先判断是不是自己锁住了这个锁,如果是则简单的增加递归计数就OK了。否则,则试图锁住owner,使用的仍然是CAS操作。如果还失败,那么则分配内核对象进行等待(调用WaitForSingleObjectEx)。注意这里并不是使用内核对象一等了之,而是让内核对象等待相对较短的时间然后再去轮询的方式进行的。每次等待间隔最大间隔为100毫秒。

 

好,再看看SSCLI中的实现。SSCLI中的思路和Mono中是一样的(或者反过来说也可以,呵呵,避免口水战的出现)。首先,在需要时初始化同步对象是在ObjHeader::GetSyncBlock()方法中实现的。接下来的操作是在AwareLock::Enter()中实现的,思路与上面说的一致。(源代码参见:<install dir>\clr\src\vm\sycnblk.h与sycnblk.cpp文件)

 

因此问题就是,Cs与Monitor有避免直接进入内核模式来使用真正的同步对象的机制,这样,如果线程对共享资源的操作够快,则可以有效的避免切换带来的开销。

但是如果还想进一步降低切换的概率呢?那就是之前先行进行一些Spin等待。这也就是前一篇文章中“优化”的意义。

posted @ 2008-07-31 22:38  TW-刘夏  阅读(1828)  评论(7编辑  收藏  举报