Net托管世界的应用程序域和线程
Managed?Host??CLR(Common Language Runtime)? AppDomain?Assembly Thread
一、?引子
.Net框架提供了全新的計算平臺,給出了一致性的面向對象的編程環境,解決了安全、版本控制等原來系統平臺中存在的問題,通過公用語言運行庫(CLR)提供了一個高效、安全的程序執行環境,也就是托管(也稱作受控,Managed)環境。在這個類似虛擬機環境下,我們編寫的程序是如何運行、如何“托管”的呢?這個托管的世界如何同非托管的世界相互聯系呢?
二、?如何進入“托管”世界
首先,我們要了解,到目前為止,還沒有“純天然的”.net執行環境(不排除類似Java芯片的.net芯片將來會有),所謂托管的環境(CLR)需要運行在當前已存非托管的系統上。要進入托管的.net世界,需要有一個稱為宿主(Host)的程序為將要運行的.net托管代碼準備執行環境—也就是要加載.net世界的基礎CLR。在目前的windows系統上,能夠擔負這個重任的有3類已存程序:
1、?shell(通常是Explorer),提供從用戶桌面啟動.net程序,創建一個進程,啟動此進程建立CLR
2、?瀏覽器宿主(Internet Explorer),處理從web下載的.net代碼執行。
3、?服務器宿主(如IIS的輔助進程aspnet_wp.exe)
在執行任何托管代碼之前,宿主必須首先加載并初始化公共語言運行庫。假設一個.net可執行程序(prj1.exe)從shell啟動,操作系統會首先建立一個進程,也就是宿主進程。裝載的程序文件包括了在執行配置信息和執行代碼,代碼入口通常會被(創建此.net應用程序的編譯器)放置一個 Stub,這個Stub實際上就是一個6字節的本機代碼:
??jmp ?_CorExeMain
而_CorExeMain是從外部庫MSCorEE.dll導出,由prj1.exe引入的函數,于是操作系統會裝入MSCorEE.dll(進入.net世界的序曲),修正_CorExeMain的運形時實際位置。
MSCorEE.dll實際上是一個COM組件庫。調用_CorExeMain后開始初始化CLR,并察看prj1.exe的CLR相關數據結構,確定執行.net托管代碼的入口。宿主調用.net 支持API CorBindToRuntimeEx來裝載CLR,并且根據配置初始化CLR的運行特征,譬如垃圾回收策略等,這是因為不同的宿主面臨的應用需求不一樣,一個服務宿主同普通的工作站宿主的“垃圾回收”(GC)機制顯然不一樣,所以啟動CLR時的參數也不一樣。
宿主裝載的是一個符合COM規范要求的組件庫文件MSCorEE.dll,也就是CLR,一般存在于操作系統目錄(便于裝載)。有關mscoree.dll更多的了解,建議可看看FrameworkSDK目錄中的頭文件mscoree.h。MSCorEE.dll通過提供啟動CLR的接口給宿主,譬如ICorRuntimeHost接口可用來配置運行庫的各個方面(如垃圾回收),以將其加載到進程中或注冊附加的事件,其中的start/stop方法可以讓宿主控制CLR在宿主進程的生存期。
其實我們可以利用CorBindToRuntimeEx編寫實現自己的宿主,關于.net宿主的實現可以單獨作為一個題目,在此給出幾個URL供大家參考。
?http://www.codeproject.com/dotnet/simpleclrhost.asp
http://www.elitevb.com/content/print.aspx?contentid=95
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/grfuncorbindtoruntimeex.asp
至此,我們明白, CLR——無所不能的虛擬機以DLL形式“寄生”于某個非托管世界的進程!所謂托管世界實際上實指在Mscoree.dll建立的可控制環境下。難怪精通VCL的用戶說CLR就是MS版的VCL!
?
三、?應用程序域(AppDomain)和域中的線程(Thread)
一旦CLR加載并初始化完成,即宣告進入.net世界。
在執行第一個入口函數(通常是編譯時指定的Main函數)之前,CLR會檢測該函數引用到的所有類型,并且由CLR從堆中申請一個內部數據結構,通過這個數據結構CLR管理所有引用到的類型的訪問,這也就是托管的根本機制。每一個類型的方法都會有一個條目供檢索和引用,在條目中保存該方法的代碼位置。在第一次執行某個函數時,屬于CLR服務的JITCompiler函數被調用,負責根據條目得到代碼,且將所有IL代碼驗證后編譯成本機代碼。.net中每一個對象無論大小,都存在一個在當前CLR中的唯一hash碼,可以將此hash碼作為類似數據庫關鍵字段來區分代表每一個對象,.net的基類Object. GetHashCode()可以幫助獲得當前對象的hash碼。如果以后調用此方法函數,那么將會直接調用這些本機代碼。此過程就是所謂的“即時編譯”。每一個托管對象的方法都會在調用前被CLR的Jit機制編譯成本地代碼,然后交給操作系統調度CPU執行。理解托管代碼在運行時的細節,可以幫助我們深入了解域和線程的本質以及他們相互之間的關系。
所謂IL驗證主要做安全檢查,大體上是根據托管代碼元數據及CLR規則來檢驗IL代碼,譬如某個變量是否初始化,調用某個函數時攜帶的參數是否正確,方法調用是否總能夠返回等等。通常情況下,IL驗證會在遇到它認為不安全的代碼時拋出VerificationException異常,阻止代碼繼續執行。
初始化同時,CLR建立第一個AppDomain,即默認應用程序域,在此域中執行入口代碼。從概念上講,應用程序域是.net 托管世界中的“應用程序在其中執行的獨立環境”,是要執行或引用的多個程序集的容器(一個應用程序域肯定不止加載一個程序集)。千萬不要理解成進程的概念,應用程序域存在于CLR中,而CLR屬于宿主進程,應用程序域同進程屬于不同層次上的概念。但的確,.net的設計者是仿照操作系統的進程概念來設計應用程序域的,使得AppDomain成為.net世界的執行單位,相互之間代碼執行隔絕。大體上,下圖可以幫助理解宿主、CLR、應用程序域之間的關系。
?
圖一
為了管理裝載的程序集,每一個應用程序域都有自己的配置信息和存儲區域、引用、執行邊界,另外重要的是有自己的安全策略。安全策略存儲在證據(evidence)中,所有在同一AppDomain內的程序集都會共享這些資源和安全指示信息。前面講到的IL驗證會根據這些配置、程序集中的元數據和證據檢驗代碼,確保應用程序域的代碼不會“越界”,不會破壞另外域的對象,也確保不會有C++世界中可怕的無效、錯誤指針的危險。
通常情況下,只有一個應用程序域的.net程序大多數行為類似傳統的非托管世界而執行,在默認應用程序域結束后,進程結束,CLR像其他DLL一樣被卸載。然而如果存在多個域,那么情況就要復雜些——起碼,CLR必須等到所有的應用程序域都結束后才可能按照DLL規則釋放。另外,不像進程間那樣通信艱難,同一個操作系統進程內的應用程序域間可以進行花費較低的交互,但是受限應用程序域的分隔特性,不可以在一個應用程序域直接操作另外一個域的實例對象(對象同程序集、應用程序域緊密相關,CLR會“照管”他們,確保應用程序域代碼執行安全)。
域間的對象引用有兩種情況:如果域間引用對象自身是傳值的,那么對象必須支持序列化(實現接口System.ISerializable), 在穿越域引用被另外域的對象時,會被序列化,到達目標域后反序列化,此時,會因反序列化從而加載該對象定義所在的程序集。如果是傳遞引用類型的對象,在目的域(需要引用該對象的域,非生成此對象的容器域)的邊界會建立一個實例對象的代理,原來的對象仍然存在,“安然”存活于被引用域,但是彼域中的實例對象代理(也就是封裝器wrapper)知道如何同此對象實例交流,此代理是通過CLR的提供的服務來調度實例對象的方法。如果對象并非支持序列化的傳值或者支持傳引用的類型,或者由于加載對象定義的所在程序集失敗(該程序集無法定位或者由于應用程序域安全限制而無法加載),會導致拋出異常,通常表示這是一個不合法的跨域對象引用操作。盡管可能所有的應用程序域都是存在于同一個操作系統進程,但是仍然由于CLR對于應用程序域的安全隔離而導致一些損耗,如果沒有必要,要盡可能避免這樣做。
應用程序域是.net CLR世界的輕量子進程,而線程卻是操作系統分配處理器時間的基本單元。線程們根據一定的優先級規則被操作系統調度,切換時都需要保持線程的執行上下文。所謂上下文是使線程在線程的宿主進程地址空間中無縫地繼續執行所需的所有信息,包括線程的 CPU 寄存器組和堆棧。在應用程序域同樣可以在主線程外創建多個子線程,.Net的Thread 和ThreadPool類提供了對操作系統的線程的包裝,大大簡化了對于線程的使用難度。當我們在一個應用程序域的主線程創建了其他子線程時,這些線程屬于當前進程。從操作系統角度來看,線程無所謂專屬于某個應用程序域,一個.net程序創建的進程中的線程實際上都是屬于同一個進程的,這也導致線程不必唯一存在于某個應用程序域中,而是可以根據需要在線程執行生命期間處于多個應用程序域(但任一時刻只能屬于某一個特定域)。應用程序域和線程不存在一對一的關系,一個線程可以跨越多個應用程序域,而一個應用程序域可以通過加載程序集的類實例對象而創建多個線程。應用程序域的方法調用、對象解析、IL的驗證執行等受CLR的控制,所以應用程序域之間可以做到相互阻隔。但線程屬于操作系統調度的,沒有應用程序域的負擔,可以迅速切換被系統調度執行。
當有需要線程從一個域訪問另外一個域的對象或者執行另外域的對象的代碼時,實際上屬于域間通信問題。因此,會在當前域創建一個ObjectHandle類型的代理(見上面的討論),然后利用ObjectHandle的成員方法Unwrap得到目標對象的引用,通過引用執行目標代碼。由于所有的托管代碼執行實際上都經過了CLR的檢查,此時,CLR會判斷到域間操作是同一個(宿主)進程,因而允許當前線程代碼執行,執行目標域的對象的方法代碼,從而實現了“穿越”域。實際上,如果發生對象域間調/引用的域不是屬于同一個進程,那么域間的通信采用.net Remotting技術而不是直接通過當前線程代碼跳轉到另一個域。如果拋開CLR的角度,從操作系統角度看,這一切純粹是普通的進程內部的線程調度。域間的“消耗”完全是因為CLR這個.net世界的“上帝”在檢查代碼的安全執行(但是是必須的),具體的細節大致是:因為所有托管的對象、方法之類的都在CLR中存在條目,執行一個對象的方法,會自動由CLR查找條目,受到CLR的管理監控,然后決定執行本地代碼。CLR判斷出當前條目屬于哪個域,就將域的私有存儲和資源分配給當前線程(線程利用靜態方法AppDomain.CurrentDomain可以得到當前所處應用程序域對象)。實際上,涉及到跨域的引用和執行,應用程序域之間的交互,無論是同一個進程的多個域還是不同進程間的域,乃至不同機器間域,通信通常根據域的分布區分對待,由于CLR能夠區分要跳轉執行的代碼是同一進程還是同一機器,所以同一進程的跨域線程調度實際上使得線程在邊界代理經過CLR的檢查后就跳轉到目標域,執行相應的本機代碼。
我們可以結合下圖進一步理解線程同域之間的關系:
?
??注意圖中其中Thread2在生命期間“穿越“了主AppDomain和多個子AppDomain。Thread3整個生命周期卻只存在AppDomain1中。AppDomain1有3個線程運行,Thread1和Thread2執行期間轉移到AppDomain2執行,其間Thread1和Thread2通過代理執行引用對象的代碼,從而將線程切換到AppDomain2。Thread2最初從默認域執行,執行期間跨越默認域、AppDomain1和AppDomain2。
可能影響代碼執行流的還有一個因素——異常。當應用程序域需要被卸載時,應用程序域的所有線程會被CLR通知,CLR會在當前域的線程強行發出一個ThreadAbortException異常,迫使他們退出該應用域。另一方面,CLR會檢查所有涉及到此域的其他域中的代理對象,將其設定為無效。以后,凡是想通過這些代理實現其他域對象的調用或引用,將會引發AppDomainUnloadedException異常。應用程序域的卸載可以由自身或者自身的子線程發出,也可以由其他域的代碼發出(譬如在一個主應用程序域創建子域后然后卸載子域)。如果由于種種原因不能夠卸載應用程序域,那么會產生CannotUnloadAppDomainException異常。如果是應用程序域自身(包括應用程序域創建的子線程)發出卸載命令,那么會由CLR來創建一個新的線程執行卸載應用程序域,產生的異常由CLR發出的線程捕捉;如果是另外的域中代碼發出卸載命令,那異常會轉交給發出卸載命令的線程。清楚了解異常的拋送路線,可以確保我們寫出安全可靠穩定的應用程序。
四、?AppDomain小結
實際上我們看到,.net的托管環境CLR是通過將COM DLL文件msCorEE.dll裝入當前操作系統進程來建立的。托管世界的執行對象提供元數據和IL代碼以及安全證據等在CLR的內存對執行,所有的托管代碼經過Jit編譯成本地代碼,由于CLR的一切對象被“托管”可以確保AppDomain的實現類似進程環境的執行分隔作用,CLR可以根據應用程序域的邊界阻止任何不安全的訪問。CLR也跟蹤管理托管線程,線程可以通過域間的通信功能實現線程在多個應用程序域上的移動。進程和線程屬于操作系統的調度執行單元,但應用程序域屬于.net的執行邏輯單元,通過“托管”實現應用程序域的代碼執行以及域間數據訪問的分隔。
通常情況下,我們的.net應用程序僅僅需要一個缺省的應用程序域,但也有一些情況考慮建立其他應用程序域可能更好一些:
1、? 需要隔離的程序集,譬如一些特別容易引起崩潰的代碼可以考慮單獨運行于一個特定的AppDomain
2、? 不同安全級別的程序集,如果需要為自己的代碼劃分安全執行的邊界,可以考慮將不同安全級別的代碼單獨創建于某個設定了不同安全信息的appDomain
3、? 從性能上考慮,有些程序集可能會消耗大量資源,盡管在托管環境下,基本上不存在資源消耗漏洞,但是總會存在特定時間訪問密集造成消耗大量資源的情況,這時可以考慮創建單獨的AppDomain,在資源消耗超過臨界點后進行AppDomain的卸載,適應系統運行要求。
4、? 不同版本的同一應用程序集的同時運行。這個在COM時代是一個大問題,現在通過AppDomain,實現了在一個進程中執行版本不同的兩個程序集,可以做到良好的兼容性。
5、動態加載一些程序。可以將一些不經常使用的程序集動態載入,(為了效率)經常使用的程序集則可以動態加載,甚至單獨加載到應用程序域中去。
6、共用程序集,提高.net的執行效率。譬如我們用到的System.object System.Int32 等對象往往多個應用程序域都需要,會造成資源浪費,為減少資源使用,含有這些常用.net類的程序集MSCorLib.dll會以單獨的中立域的方式加載,CLR會為其維護一個特殊的加載器,使得這個程序集只有在進程中斷時才會被卸載,從而提高了速度,減少了內存等資源的浪費。
五、?參考
本文大致討論了.net CLR托管的執行機制,以及程序集如何加載到應用程序域執行、線程等如何在托管集之下的執行,試圖幫助讀者理解托管的計算環境代碼執行的較為詳細情形。但是,并沒有試圖詳細闡述.net框架的體系細節,甚至連具體的.net代碼都沒有提供分析,如果您需要進一步了解.net的更多細節,請參考閱讀以下的資料:
1、《.net 框架程序設計(修訂版)》 Jeffrey Richter 著/李建中?譯?清華大學出版社。
2、《.net核心技術—原理與架構》?劉曉華 編著 電子工業出版社
3、MSDN .net 框架SDK
轉載于:https://www.cnblogs.com/hellolong/articles/5746741.html
總結
以上是生活随笔為你收集整理的Net托管世界的应用程序域和线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于创业公司产品开发原则
- 下一篇: this、new、模式工厂、创建新的构造