CLR線程概覽(下)

做者:施懿民
連接:https://zhuanlan.zhihu.com/p/20866017
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

同步: 託管代碼

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

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

由於任意一個對象都是潛在的鎖/條件變量,每一個對象都須要有一個地方用來保存鎖信息。這個就是在「對象頭(object headers)」和「同步塊(sync blocks)」裏完成的。安全

對象頭是一個在每一個託管對象前面機器字長大小的字段。它在不少地方會用到,例如保存對象的哈希值。其中一個目的就是保存對象的鎖狀態。若是對象頭須要保存更多的信息,咱們經過建立一個「同步塊」的方式擴充對象。服務器

同步塊保存在同步塊表(Sync Block Table)裏,經過同步塊索引來尋址。對象的同步塊索引保存在對象頭裏。ide

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

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

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

一個用來做爲條件變量的對象(經過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線程池維護一個託管線程集合用來執行用戶的「工做」。這些託管線程都綁定到線程池管理的原生線程。線程池還維護一小部分的原生線程來處理相似「線程注入」,定時器以及「已註冊的等待」等等功能。

相關文章
相關標籤/搜索