託管代碼在「託管線程」上執行,(託管線程)與操做系統提供的原生線程不一樣。原生線程是在物理機器上執行的原生代碼序列;而託管線程則是在CLR虛擬機上執行的虛擬線程。git
正如JIT解釋器將「虛擬的」中間(IL)指令映射到物理機器上的原聲指令,CLR線程基礎架構將「虛擬的」託管線程映射到操做系統的原生線程上。github
在任意時刻,一個託管線程可能會也可能不會被分配到一個原生線程執行。例如,一個已經被建立(經過「new System.Threading.Thread」)可是未啓動(經過「System.Threading.Thread.Start」)的託管線程不會被指派到原生線程上執行。相似的,雖然CLR在實際上不會這樣作,可是一個託管線程在執行時可被切換到多個原生線程上執行。安全
託管代碼裏公開的Thread接口就是用來隱藏其底層原生線程的細節的:數據結構
CLR提供並實現了託管線程的抽象。好比說,雖然其不暴露操做系統的線程本地存儲(TLS)機制,可是其提供了託管「線程靜態」變量。相似的,雖然其不提供原生線程的「線程ID」,可是其提供與操做系統無關的「託管線程ID」。不過爲了便於診斷問題,底層原生線程的一些細節能夠經過System.Diagnostics命名空間裏的類型得到。架構
託管線程還提供了原生線程一般不用的功能。第一,託管線程在堆棧上使用GC引用,這樣CLR必須在GC的時候能夠枚舉(甚至可能修改)這些GC引用。爲了實現這個目的,CLR必須「暫停」每一個託管線程(即中止執行以即可以發現全部的GC引用)。第二,當AppDomain卸載時,CLR必須保證沒有線程在執行這個AppDomain裏的代碼。這要求CLR能夠強制線程從AppDomain脫離,CLR經過在線程裏注入ThreadAbortException來實現這點。函數
每一個託管線程都跟一個Thread對象關聯,其在threads.h裏定義。這個對象跟蹤CLR關於託管對象所須要瞭解的全部東西。包括如線程的當前GC模式和堆棧幀鏈這些必需品,也包括爲了性能因素建立的不少元素(如一些快速arena-style分配器)。oop
全部的Thread對象都保存在ThreadStore中(也在threads.h中定義),其時一個全部已知線程的列表。要遍歷全部的託管線程,須要先獲取ThreadStoreLock,再使用ThreadStore::GetAllThreadList來枚舉全部的線程對象。這個列表也包含沒有被指派原生線程的託管線程(如未啓動的線程,或原生線程已經存在了)。佈局
原生線程能夠經過一個原生線程本地存儲(TLS)槽來獲取綁定到該原生線程的託管線程。這容許原生線程上運行的代碼能夠經過GetThread()獲取對應的Thread對象。性能
另外,許多託管線程有一個與原生Thread對象相區別的 託管 Thread對象(System.Threading.Thread)。託管Thread對象提供了方法以便託管代碼與線程交互,其大部分是原生Thread對象功能的封裝。經過Thread.CurrentThread能夠(在託管代碼中)獲取到當前的託管線程對象。操作系統
在調試器裏,「!Threads」這個SOS擴展命令能夠用來枚舉ThreadStore裏的全部Thread對象。
一個託管線程在下列這些情形中建立:
在#1和#2這些情形中,CLR負責建立支撐託管線程的原生線程。這個只會在線程實際上啓動了纔會發生。在這些情形裏,CLR「負責」原生線程;CLR負責原生線程的生命週期,因爲CLR建立了它,所以也就知道線程的存在。
在#3和#4這些情形裏,原生線程在託管線程以前就存在了,並且由CLR以外的代碼負責。CLR不負責這種原生線程的生命週期。CLR只是在其第一次調用託管代碼時意識到其存在。
當一個原生線程結束時,CLR經過其DllMain函數得到通知。這在操做系統的「加載鎖」中發生,因此在處理這個通知的時候只能作不多(安全)的事情。與其銷燬與託管線程關聯的數據結構,這個線程只是被簡單地標識成「死亡」狀態,並啓動finalizer線程。finalizer線程會遍歷ThreadStore裏全部死亡且託管代碼再也不使用的線程。
CLR必須能夠找到託管對象的全部引用以便執行GC。託管代碼一直在不停的訪問GC堆,操做堆棧和寄存器上的引用。CLR必須保證全部線程停在安全可靠的位置(這樣他們不會修改GC堆),以便找到全部的託管對象。它只會停在安全點,這個時候能夠在寄存器和堆棧上檢查全部可用的引用。
另外一個辦法就是GC堆、每一個線程的堆棧和寄存器狀態都是所謂的「共享狀態」,可被多個線程訪問。正如大多數共享狀態同樣,須要一些「鎖」來保護它們。託管代碼在訪問堆以前必需要獲取鎖,而且在安全的時候釋放鎖。
CLR將這種「鎖」稱做線程的「GC模式」。當線程獲取鎖的時候,處於「合做模式(cooperative mode)」;其必須與GC「合做」(經過釋放鎖)才能容許進行垃圾回收。而線程沒有獲取鎖的時候,處於「優先模式(preemptive mode)」 - GC能夠「優先」進行垃圾回收,由於其知道線程沒有訪問GC堆。
GC只有在全部線程都處於「優先」模式(即沒有獲取鎖)時才能進行垃圾回收。將全部線程移到優先模式的過程就稱爲「GC懸停(GC suspension)」或「暫停執行引擎」。
一個不大成熟的實現「鎖」的方案是要求每一個託管線程在訪問GC堆的時候實際獲取和釋放保護它的鎖。而後GC會向每一個線程嘗試獲取鎖,一旦其獲取全部線程的鎖,就能夠安全的進行垃圾回收了。
然而,上面的方案由於兩個緣由而顯得不足。第一,這會要求託管代碼耗費大量的時間在於獲取和釋放鎖(或至少是檢查GC是否在嘗試獲取鎖 - 也就是「GC輪詢 GC poll - 即不停的向GC輪詢」)。第二,它要求JIT解釋器生成大量的「GC信息代碼」,以描述每一行JIT生成的代碼後的堆棧的佈局和寄存器狀態,這些信息會耗費大量的內存。
咱們針對上述辦法的改進方案是,將JIT後的託管代碼區分紅「部分可中斷」和「所有可中斷」的代碼。在部分可中斷代碼中,調用其餘函數的地方是惟一的安全點,且JIT生成顯式的「GC輪詢」點以便檢查是否有等待的GC。(JIT)只須要在這些地方生成GC信息。在所有可中斷代碼裏,每一個指令都是一個安全點,JIT爲每一個指令生成GC信息 - 可是其不生成「GC」輪詢代碼。所有可中斷代碼而是經過劫持線程(該過程在後文講解)來進入「中斷」狀態。JIT基於代碼質量,GC信息的大小以及GC懸停的時間延遲這些因素來斷定是產生所有或部分可中斷代碼。
基於上述信息,定義了三個基礎操做:進入合做模式,離開合做模式以及暫停執行引擎。
一個線程經過調用Thread::DisablePreemptiveGC進入合做模式。其爲當前線程獲取「鎖」:
兩個步驟其實是原子操做。
一個線程經過調用Thread::EnablePreemptiveGC來進入優先模式(釋放鎖)。其經過標識線程再也不進入合做模式來完成,並通知GC線程能夠啓動執行。
當GC開始運行時,第一步就是中斷執行引擎。GCHeap::SuspendEE函數就是用來幹這個的:
爲了GC懸停而進行的劫持操做是經過Thread::SysSuspendForGC函數完成的。這個函數經過強制全部運行在合做模式的託管線程在「安全點」離開合做模式。其經過枚舉全部的託管線程(經過遍歷ThreadStore),針對每一個運行在合做模式中的託管線程:
爲了卸載一個應用程序域(AppDomain),CLR須要保證沒有線程運行在這個應用程序域中。爲了實現這點,全部託管線程都被枚舉,而任何堆棧上有屬於被卸載應用程序域的幀的線程都被「中斷」。一個ThreadAbortException異常被注入正在運行的線程,並致使線程向上展開(一直運行拆除代碼)直到沒有運行在這個應用程序域當中的堆棧幀,而ThreadAbortException也被轉換成一個AppDomainUnloaded異常。
ThreadAbortException是一個很特別的異常。其也許會被用戶代碼捕捉到,可是CLR確保其在用戶的異常處理代碼以後再次被拋出。所以ThreadAbortException有時被稱做「沒法被捕捉」的,儘管嚴格來講不是這樣的。
ThreadAbortException一般經過在託管線程上設置一個標誌位標誌其「正在終止」來拋出的。CLR不少地方都會檢查這個標誌位(特別要注意的,每次從p/invoke返回),而且常常有設置這個標誌位的目的就是爲了讓線程及時終止的情形。
然而,好比說,線程正在運行一個長時間的託管循環,那麼它可能根本不會檢查這個標誌位。爲了讓這樣的線程快速終止,線程就被「劫持」並強制拋出ThreadAbortException異常。劫持過程跟GC懸停很相似,只是線程跳轉過去的代碼塊拋出ThreadAbortException,而不是等待GC運行完畢。
這種劫持意味着ThreadAbortException可能在任意位置發生。這樣使得託管代碼很難正確處理ThreadAbortException異常。所以除了在卸載應用程序域的時候使用這種機制之外 - 保證由ThreadAbort損壞的狀態都跟應用程序域一塊兒被清理,在其餘地方使用它都不是很明智的選擇。