同步: 托管代碼
托管代碼可以訪問很多在System.Threading里定義的同步原語株搔。包括操作系統(tǒng)原語的簡單封裝如:互斥(Mutex)噩翠,事件(Event)和旗標(Semaphore)對象狈涮,也包括類似的柵欄(Barrier)和自旋鎖(SpinLock)等抽象颤介。但托管代碼用的最多的同步機制是System.Threading.Monitor掸犬,其提供了針對 任意托管對象 的高性能同步鎖機制帝雇,還提供了被其保護的狀態(tài)發(fā)生變化時的通知機制的“條件變量”語義聋伦。
Monitor是通過一個“混合鎖”來實現(xiàn)的夫偶,其有自旋鎖和類似互斥(Mutex)這些基于操作系統(tǒng)內(nèi)核鎖的功能界睁。這個思路源自于大部分鎖都是短暫獲取的,因此自旋等待鎖被釋放的所耗費的時間比調(diào)用內(nèi)核API從而阻塞線程更少兵拢。當然將CPU的時鐘周期浪費在自旋上也是很嚴重的翻斟,因此如果鎖在一段時間內(nèi)沒有被釋放的話,那么CLR則會退回到調(diào)用內(nèi)核API的實現(xiàn)上说铃。
因為任意一個對象都是潛在的鎖/條件變量访惜,每個對象都需要有一個地方用來保存鎖信息。這個就是在“對象頭(object headers)”和“同步塊(sync blocks)”里完成的腻扇。
對象頭是一個在每個托管對象前面機器字長大小的字段债热。它在很多地方會用到,例如保存對象的哈希值幼苛。其中一個目的就是保存對象的鎖狀態(tài)窒篱。如果對象頭需要保存更多的信息,我們通過創(chuàng)建一個“同步塊”的方式擴充對象舶沿。
同步塊保存在同步塊表(Sync Block Table)里墙杯,通過同步塊索引來尋址。對象的同步塊索引保存在對象頭里括荡。
關于對象頭和同步塊的細節(jié)在syncblk.h/.cpp里定義霍转。
如果對象頭里還有空間,Monitor將鎖住對象的線程的托管線程ID(如果沒有線程鎖住對象則是0)保存在其中一汽。在這種情形下避消,獲取鎖的過程其實就是自旋等待對象頭的線程ID為0,然后原子操作設置其值為當前線程的托管線程ID召夹。
如果自旋一些次數(shù)后還不能獲取鎖岩喷,或?qū)ο箢^已經(jīng)用作其它目的,那么就會為這個對象創(chuàng)建同步塊监憎。它包含一些額外數(shù)據(jù)纱意,包括用來阻塞當前線程的事件對象,這樣運行我們停止自旋并等待鎖被釋放鲸阔。
一個用來作為條件變量的對象(通過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)建了這個線程用來運行托管代碼。當GC決定一個可終結(jié)(finalizable)的對象不再被引用寝衫,其將該對象置于終結(jié)隊列顷扩。當GC結(jié)束后,終結(jié)者線程會被喚醒并處理隊列里的所有終結(jié)對象慰毅。對象一個一個出列隘截,其終結(jié)(finalizer)函數(shù)被依次調(diào)用。
該線程還用來處理一些CLR內(nèi)部的清理工作汹胃,并等待一些外部事件通知(如低內(nèi)存情形下婶芭,GC會被告知盡量兇悍的回收垃圾)。詳情請參見GCHeap::FinalizerThreadStart着饥。
GC 線程
當運行在“并行”或“服務器”模式時犀农,GC創(chuàng)建一個或多個后臺線程來并行執(zhí)行垃圾回收的不同階段。這些線程完成由GC管理宰掉,而且永遠不會執(zhí)行托管代碼鞭盟。
調(diào)試器線程
CLR為每個托管進程維護了一個原生線程,其用來在附加到托管調(diào)試器時執(zhí)行多個調(diào)試操作局嘁。
應用程序域卸載線程
這個線程負責卸載應用程序域奠衔。其通過一個單獨的CLR內(nèi)部線程,而不是在請求卸載應用程序域的線程里完成戚绕。因為 a) 為卸載過程提供受保證的堆椢谱空間,b) 在必要時允許請求卸載的線程從應用程序域里向上展開舞丛。
線程池線程
CLR線程池維護一個托管線程集合用來執(zhí)行用戶的“工作”耘子。這些托管線程都綁定到線程池管理的原生線程。線程池還維護一小部分的原生線程來處理類似“線程注入”球切,定時器以及“已注冊的等待”等等功能谷誓。