CLR線程概覽(下)

同步: 托管代碼

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

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

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

對象頭是一個在每個托管對象前面機器字長大小的字段。它在很多地方會用到,例如保存對象的哈希值。其中一個目的就是保存對象的鎖狀態(tài)。如果對象頭需要保存更多的信息,我們通過創(chuàng)建一個“同步塊”的方式擴充對象。

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

關(guān)于對象頭和同步塊的細節(jié)在syncblk.h/.cpp里定義。

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

如果自旋一些次數(shù)后還不能獲取鎖,或?qū)ο箢^已經(jīng)用作其它目的,那么就會為這個對象創(chuàng)建同步塊。它包含一些額外數(shù)據(jù),包括用來阻塞當(dāng)前線程的事件對象,這樣運行我們停止自旋并等待鎖被釋放。

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

同步: 原生情況

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

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

除了上述細節(jié)之外,GC懸停是一個特殊的“鎖”,而且?guī)缀跤绊慍LR的方方面面。如果必須處理GC堆上的對象,虛擬機的原生代碼可能要進入“合作”模式,這樣“GC懸停鎖”就變成原生虛擬機代碼里最重要的同步機制,在托管世界里也一樣。

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

GC 模式

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

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

需要注意的是GCX_COOP從GC堆上獲取一個鎖。在線程處于合作模式時,不能執(zhí)行GC。而且原生線程也不能像托管線程那樣被“劫持”,因此線程在切換回優(yōu)先模式時都是處于合作模式。

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

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

代碼規(guī)范 文檔描述了安全進行GC模式切換的必要原則。

Crst

正如Monitor對象是托管代碼里推薦的鎖機制,Crst是虛擬機代碼里的推薦機制。與Monitor類似,Crst是一個知道宿主和GC模式的混合鎖。Crst通過“層級鎖”機制來規(guī)避死鎖,該實現(xiàn)可參考 BotR的層級鎖章節(jié).

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

特殊線程

除了托管代碼創(chuàng)建的托管線程,CLR自身還創(chuàng)建了一些“特殊”線程。

終結(jié)者(Finalizer)線程

每個進程都創(chuàng)建了這個線程用來運行托管代碼。當(dāng)GC決定一個可終結(jié)(finalizable)的對象不再被引用,其將該對象置于終結(jié)隊列。當(dāng)GC結(jié)束后,終結(jié)者線程會被喚醒并處理隊列里的所有終結(jié)對象。對象一個一個出列,其終結(jié)(finalizer)函數(shù)被依次調(diào)用。

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

GC 線程

當(dāng)運行在“并行”或“服務(wù)器”模式時,GC創(chuàng)建一個或多個后臺線程來并行執(zhí)行垃圾回收的不同階段。這些線程完成由GC管理,而且永遠不會執(zhí)行托管代碼。

調(diào)試器線程

CLR為每個托管進程維護了一個原生線程,其用來在附加到托管調(diào)試器時執(zhí)行多個調(diào)試操作。

應(yīng)用程序域卸載線程

這個線程負責(zé)卸載應(yīng)用程序域。其通過一個單獨的CLR內(nèi)部線程,而不是在請求卸載應(yīng)用程序域的線程里完成。因為 a) 為卸載過程提供受保證的堆??臻g,b) 在必要時允許請求卸載的線程從應(yīng)用程序域里向上展開。

線程池線程

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容