到底差在了什么地方: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等待。這也就是前一篇文章中“優化”的意義。
雖然我們整篇都在討論.NET下的Multi-threading的問題,但是實際上很多問題都是可以類推的。例如前幾次我們反復的說道了關于CriticalSection的問題。說它比MUTEX有何優越之處,例如速度就是一個明顯的優勢。但是從留言中發現在這個問題上存在著一些誤會。今天不妨就閑扯一下這個問題。
浙公網安備 33010602011771號