標籤:

CLR線程概覽(下)

同步: 託管代碼

託管代碼可以訪問很多在System.Threading里定義的同步原語。包括操作系統原語的簡單封裝如:互斥(Mutex),事件(Event)和旗標(Semaphore)對象,也包括類似的柵欄(Barrier)和自旋鎖(SpinLock)等抽象。但託管代碼用的最多的同步機制是System.Threading.Monitor,其提供了針對 任意託管對象 的高性能同步鎖機制,還提供了被其保護的狀態發生變化時的通知機制的「條件變數」語義。

Monitor是通過一個「混合鎖」來實現的,其有自旋鎖和類似互斥(Mutex)這些基於操作系統內核鎖的功能。這個思路源自於大部分鎖都是短暫獲取的,因此自旋等待鎖被釋放的所耗費的時間比調用內核API從而阻塞線程更少。當然將CPU的時鐘周期浪費在自旋上也是很嚴重的,因此如果鎖在一段時間內沒有被釋放的話,那麼CLR則會退回到調用內核API的實現上。

因為任意一個對象都是潛在的鎖/條件變數,每個對象都需要有一個地方用來保存鎖信息。這個就是在「對象頭(object headers)」和「同步塊(sync blocks)」里完成的。

對象頭是一個在每個託管對象前面機器字長大小的欄位。它在很多地方會用到,例如保存對象的哈希值。其中一個目的就是保存對象的鎖狀態。如果對象頭需要保存更多的信息,我們通過創建一個「同步塊」的方式擴充對象。

同步塊保存在同步塊表(Sync Block Table)里,通過同步塊索引來定址。對象的同步塊索引保存在對象頭裡。

關於對象頭和同步塊的細節在syncblk.h/.cpp里定義。

如果對象頭裡還有空間,Monitor將鎖住對象的線程的託管線程ID(如果沒有線程鎖住對象則是0)保存在其中。在這種情形下,獲取鎖的過程其實就是自旋等待對象頭的線程ID為0,然後原子操作設置其值為當前線程的託管線程ID。

如果自旋一些次數後還不能獲取鎖,或對象頭已經用作其它目的,那麼就會為這個對象創建同步塊。它包含一些額外數據,包括用來阻塞當前線程的事件對象,這樣運行我們停止自旋並等待鎖被釋放。

一個用來作為條件變數的對象(通過Monitor.Wait 和 Monitor.Pulse)總是會被擴充的,因為同步塊里已經沒有足夠的空間來保存必要的狀態。

同步: 原生情況

CLR的原生部分也必須要有線程意識,因為其可能在多個線程上調用託管代碼。這樣要求原生的同步機制,例如鎖,事件等等。

ITaskHost API 允許一個CLR宿主修改託管線程的很多方面,包括線程的創建、銷毀和同步。這種允許宿主修改原生同步機制要求虛擬機的代碼不能直接使用原生的同步原語(即臨界區,互斥鎖,事件等),而是需要使用虛擬機在其上的封裝)。

除了上述細節之外,GC懸停是一個特殊的「鎖」,而且幾乎影響CLR的方方面面。如果必須處理GC堆上的對象,虛擬機的原生代碼可能要進入「合作」模式,這樣「GC懸停鎖」就變成原生虛擬機代碼里最重要的同步機制,在託管世界裡也一樣。

原生虛擬機代碼里主要用到的同步機制是GC模式和Crst。

GC 模式

如上所述,所有託管線程都在合作模式中運行,因為其可能操作GC堆。一般來講,原生代碼不會碰託管對象,因此運行在優先模式。但有些虛擬機里的原生代碼需要訪問GC堆,需要運行在合作模式。

原生代碼通常不會直接操作GC模式,而是通過兩個宏:GCX_COOP and GCX_PREEMP 來進入期望的模式,並創建「支持物」以便線程在退出範圍的時候返回到之前的模式。

需要注意的是GCX_COOP從GC堆上獲取一個鎖。在線程處於合作模式時,不能執行GC。而且原生線程也不能像託管線程那樣被「劫持」,因此線程在切換回優先模式時都是處於合作模式。

因此在原生代碼里進入合作模式是不被鼓勵的。如果必須要進入合作模式,那麼時間越短越好。線程在此模式時不能被阻塞,而且實際上不能安全的獲取鎖。

類似的,GCX_PREEMP 釋放 線程擁有的鎖。在進入優先模式之前必須要萬分小心來確保所有GC引用都被妥善保護。

代碼規範 文檔描述了安全進行GC模式切換的必要原則。

Crst

正如Monitor對象是託管代碼里推薦的鎖機制,Crst是虛擬機代碼里的推薦機制。與Monitor類似,Crst是一個知道宿主和GC模式的混合鎖。Crst通過「層級鎖」機制來規避死鎖,該實現可參考 BotR的層級鎖章節.

雖然有一些必須這麼做的異常情況,在合作模式下獲取一個Crst鎖通常是不合適的。

特殊線程

除了託管代碼創建的託管線程,CLR自身還創建了一些「特殊」線程。

終結者(Finalizer)線程

每個進程都創建了這個線程用來運行託管代碼。當GC決定一個可終結(finalizable)的對象不再被引用,其將該對象置於終結隊列。當GC結束後,終結者線程會被喚醒並處理隊列里的所有終結對象。對象一個一個出列,其終結(finalizer)函數被依次調用。

該線程還用來處理一些CLR內部的清理工作,並等待一些外部事件通知(如低內存情形下,GC會被告知盡量兇悍的回收垃圾)。詳情請參見GCHeap::FinalizerThreadStart。

GC 線程

當運行在「並行」或「伺服器」模式時,GC創建一個或多個後台線程來並行執行垃圾回收的不同階段。這些線程完成由GC管理,而且永遠不會執行託管代碼。

調試器線程

CLR為每個託管進程維護了一個原生線程,其用來在附加到託管調試器時執行多個調試操作。

應用程序域卸載線程

這個線程負責卸載應用程序域。其通過一個單獨的CLR內部線程,而不是在請求卸載應用程序域的線程里完成。因為 a) 為卸載過程提供受保證的堆棧空間,b) 在必要時允許請求卸載的線程從應用程序域里向上展開。

線程池線程

CLR線程池維護一個託管線程集合用來執行用戶的「工作」。這些託管線程都綁定到線程池管理的原生線程。線程池還維護一小部分的原生線程來處理類似「線程注入」,定時器以及「已註冊的等待」等等功能。


推薦閱讀:

.NET/CLR都開源了,VC++還遠嗎?
.NET CLR怎麼保證執行正確的unsafe代碼不掛掉?
為啥JVM等虛擬機都沒考慮實現自動矢量化處理?
垃圾內存回收演算法
RyuJIT為什麼比JIT64編譯速度快?

TAG:NET | CLR |