寄宿(hosting)使任何應用程序都能利用clr的功能。特別要指出的是,它使現(xiàn)有應用程序至少能部分使用托管代碼編寫。另外,寄宿還為應用程序提供了通過編程來進行自定義和擴展的能力。 允許可擴展性意味著第三方代碼可在你的進程中運行。在windows中將第三方dll加載到進程中意味著冒險。dll中的代碼很容易破壞應用程序的數(shù)據(jù)結構和代碼。dll還可能企圖利用應用程序的安全上下文來訪問它本來無權訪問的資源。clr的appDomain功能解決了所有這些問題。AppDomain允許第三方、不受信任的代碼在現(xiàn)有的進程中運行,而CLR保證數(shù)據(jù)結構、代碼和安全上下文不被濫用或破壞。 程序員經(jīng)常講寄宿和AppDomain與程序集的加載和反射一起使用。這4種技術一起使用,使clr稱為一個功能及其豐富和強大的平臺。本章重點在于寄宿和AppDomain。 CLR寄宿.net在windows平臺的頂部運行。這意味著.net必須用windows能理解的技術來構建。首先,所有托管模塊和程序集文件都必須使用windows PE文件格式,而且要么是windows EXE文件,要么是Dll文件。 開發(fā)clr時,Microsoft實際是把它實現(xiàn)成包含在一個dll中的com服務區(qū)。也就是說,Microsoft為clr定義了一個標準的com接口,并為該接口和com服務器分配了guid。安裝.net時,代表clr的com服務器和其他com服務器一樣在windows注冊表中注冊。 任何windows應用程序都能寄宿clr。但不要通過調(diào)用CoCreateInstance來創(chuàng)建clr com服務器的實例。相反,你的非托管宿主應該調(diào)用MetaHost.h文件中聲明的CLRCreateInstance函數(shù)。CLRCreateInstance函數(shù)在MSCorEE.dll文件中實現(xiàn),該文件一般在system32目錄中。這個dll被人們親切地稱為墊片(shim),它的工作是決定創(chuàng)建哪個版本的clr:墊片dll本身不包含clr com服務器。 一臺機器可以安裝多個版本的clr,但只有一個版本的MSCorEE.dll文件。機器上安裝的MSCorEE.dll是與機器上安裝的最新版本的clr一起發(fā)布的那個版本。 CLRCreateInstance函數(shù)可返回一個ICLRMetaHost接口。宿主應用程序可調(diào)用這個接口的GetRuntime函數(shù),指定數(shù)組要創(chuàng)建的clr的版本。然后,墊片將所需版本的clr加載到宿主的進程中。 默認情況下,當一個托管的可執(zhí)行文件啟動時,墊片會檢查可執(zhí)行文件,提取當初生成和測試應用程序時使用的lcr的版本信息。但應用程序可以在它的xml配置文件中設置requiredRuntime和supportedRuntime來覆蓋默認行為。 GetRuntime函數(shù)返回指定非托管ICLRRuntimeInfo接口的指針。有個這個指針后,就可以利用GetInterface方法獲取ICLRRuntimeHost接口。宿主應用程序可調(diào)用該接口定義的方法做如下事情 1 設置宿主管理器 2 獲取clr管理器 3 初始化并啟動clr 4 加載程序集并執(zhí)行其中的代碼 5 停止clr,阻止任何更多的托管代碼在windows進程中運行 注意:一個clr加載到winows進程之后,變永遠不能卸載;clr從進程卸載的唯一途徑就是終止進程,這會造成windows清理進程使用的所有資源。 AppDomainCLR COM服務器服務器初始化時會創(chuàng)建一個AppDomain。AppDomain是一組程序集的邏輯容器 CLR初始化時創(chuàng)建的第一個AppDomain稱為默認AppDomain,這個默認的AppDomain還有在windows進程終止時才會被注銷。 除了默認AppDomain,正在使用非托管com接口方法或托管類型方法的宿主還可要求clr創(chuàng)建額外的AppDomain。AppDomain是為了提供隔離而設計的。下面總結了AppDomain的具體功能。 1 一個AppDomain的代碼不能直接訪問另一個AppDomain的代碼創(chuàng)建的對象 一個AppDomain中的代碼創(chuàng)建了一個對象后,該對象便被該AppDomain擁有。換言之,它的生存期不能超過創(chuàng)建它的代碼所在的AppDomain。一個AppDomain中的代碼要訪問另一個AppDomain中的對象,只能使用按引用封送或者按值封送的語義。這就強制建立了清晰的分隔和邊界,因為一個AppDomain中的代碼不能直接引用另一個AppDomain中的代碼創(chuàng)建的對象。這種隔離使得AppDomain能很容易地從進程中卸載,不會影響其他AppDomain正在運行的代碼。 2 AppDomain可以卸載 CLR不支持從AppDomain中卸載特定的程序集。但可以告訴clr卸載一個AppDomain,從而卸載該AppDomain當前包含的所有程序集。 3 AppDomain可以單獨保護 AppDomain創(chuàng)建后會應用一個權限集,它決定了向這個AppDomain中運行的程序集授予的最大權限。正式由于存在這些權限,所以當宿主加載一些代碼后,可以保證這些代碼不會破壞(或讀?。┧拗鞅旧硎褂玫囊恍┲匾獢?shù)據(jù)結構。 4 AppDomain可以單獨配置 AppDomain創(chuàng)建后會管理一組配置設置,這些設置主要影響clr在AppDomain中加載程序集的方式。涉及搜索路勁、版本綁定重定向、劵影賦值以及加載器優(yōu)化。 提示:windows的一個重要特色就是讓每個應用程序都在自己的進程地址空間中運行。這就保證了一個應用程序的代碼不能訪問另一個應用程序使用的代碼或數(shù)據(jù)。進程隔離可防范安全漏洞、數(shù)據(jù)破壞和其他不可預測的行為,確保了windows系統(tǒng)以及在它上面運行的應用程序的健壯性。遺憾的是,在windows中創(chuàng)建進程的開銷很大。win32 createProcess函數(shù)的速度很慢,而且windows需要大量內(nèi)存來虛擬化進程的地址空間。但是,如果應用程序完全由托管代碼構成,同時這些代碼沒有調(diào)用非托管代碼,那么在一個windows進程中運行多個托管應用程序是沒有問題的。AppDomain提供了保護、配置和終止其中每一個應用程序所需的隔離。 圖22-1演示了一個windows進程,其中運行著一個CLR COM服務器。該CLR當前管理著兩個AppDomain(雖然在一個windows進程中可以運行的AppDomain數(shù)量沒有硬性限制)。每個AppDomain都有自己的loader堆,每個loader堆都記錄了自AppDomain創(chuàng)建以來訪問過哪些類型。Loader堆中的每個類型對象都有一個方法表,方法表中的每個記錄項都指向jit編譯的本機代碼(前提是方法至少執(zhí)行過一次)。 除此之外,每個AppDomain都加載了一些程序集。AppDomain #1(默認AppDomain)有三個程序集:myApp.exe,TypeLib.dll和System.dll。AppDomain#2有兩個程序集Wintellect.dll和system.dll。 兩個AppDomain都加載了system.dll程序集。如果這兩個AppDomain都使用來自system.dll的一個類型,那么兩個AppDomain的loader堆會為相同的類型分別分配一個類型對象:類型對象的內(nèi)存不會由兩個AppDomain共享。另外,一個AppDomain中的代碼調(diào)用一個類型定義的方法時,方法il代碼會進行jit編譯,生成的本機代碼單獨與每個AppDomain關聯(lián),而不是由調(diào)用它的所有AppDomain共享。 不共享類型對象的內(nèi)存或本機代碼顯得有些浪費。但AppDomain的設計宗旨就是提供隔離:clr要求在卸載某個AppDomain并釋放其所有資源時不會影響到其他任何AppDomain。復制clr的數(shù)據(jù)結構才能保證這一點。另外,還保證多個AppDomain使用的類型在每個AppDomain中都有一組靜態(tài)字段。 有的程序集本來就要有多個AppDomain使用。最典型的例子就是MSCorLib.dll。該程序集包含了system.object,system.int32以及其他所有.net密不可分的類型。clr初始化時,該程序集會自動加載,而且所有AppDomain都共享該程序集中的類型。為了減少資源消耗,MSCorLib程序集以一種AppDomain中立的方式加載。也就是說,針對以AppDomain中立方式加載的程序集,clr會為他們維護一個特殊的loader堆。該loader對中的所有類型對象,以及為這些類型定義的方法jit編譯生成的所有本機代碼,都會由進程中所有AppDomain共享。遺憾的是,共享這些資源所獲得的收益并不是沒有代價,這個代價就是,以AppDomain中立方式加載的所有程序集永遠不能卸載。要回收他們占用的資源,唯一的辦法就是終止windows進程,讓windows去回收資源。 跨越AppDomain邊界訪問對象一個線程能執(zhí)行一個AppDomain中的代碼,再執(zhí)行另一個AppDomain的代碼。Thread.GetDomain()方法向CLR詢問它正在執(zhí)行哪個AppDomain。AppDomain的FriendlyName屬性獲取AppDomain的友好名稱(默認AppDomain使用可執(zhí)行文件的名稱作為友好名稱) ![]() ![]() using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Remoting; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication7 { class Program { static void Main(string[] args) { Marshalling(); } private static void Marshalling() { //獲取AppDomain引用(“調(diào)用線程”當前正在該AppDomain中執(zhí)行) AppDomain adCallingThreadDomain = Thread.GetDomain(); //每個AppDomain都分配了友好字符串名稱(以便調(diào)試) //獲取這個AppDomain的友好名稱并顯示它 String CallingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("默認AppDomain友好的名稱={0}",adCallingThreadDomain); //獲取并顯示我們的AppDomain中包含了“Main”方法的程序集 String exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("包含“Main”方法的程序集={0}", exeAssembly); //定義局部變量來引用一個AppDomain AppDomain ad2 = null; //************************************************************************************************************ //************************************************************ DEMO 1:使用“按引用封送”進行跨AppDomain通信 *** //************************************************************************************************************ Console.WriteLine("{0} Demo1 按引用封送", Environment.NewLine); //新建一個AppDomain(從當前AppDomain繼承安全性和配置) ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; //將我們的程序集加載到新AppDomain,構造一個對象,把它封送回我們的AppDomain(實際得到對一個代理的引用) mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); Console.WriteLine("Type={0}", mbrt.GetType());//CLR在類型上撒謊了 //證明得到的是對一個代理對象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); //看起來像是在MarshalByRefType上調(diào)用了一個方法,實則不然。 //我們是在代理類型上調(diào)用了一個方法,代理是線程切換到擁有對象的那個 //AppDomain,并在真實的對象上調(diào)用這個方法 mbrt.SomeMethod(); //卸載新的AppDomain AppDomain.Unload(ad2); //此時,mbrt引用了一個有效的代理對象;代理對象引用一個無效的AppDomain try { mbrt.SomeMethod(); Console.WriteLine("調(diào)用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("調(diào)用失敗,AppDomain被卸載了"); } //************************************************************************************************************ //************************************************************ DEMO 2:使用“按值封送”進行跨AppDomain通信 *** //************************************************************************************************************ Console.WriteLine("{0} Demo2 按值封送", Environment.NewLine); ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); //對象的方法返回所返回對象的副本 //對象按值(而非按引用)封送 MarshalByValType mbvt= mbrt.MethodWithReturn(); //證明得到的是對一個代理對象的引用 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); //看起來在MarshalByValType上調(diào)用一個方法,實際也是如此 Console.WriteLine("Return object created " + mbvt.ToString()); //卸載新的AppDomain AppDomain.Unload(ad2); //此時,mbrt引用了一個有效的x代理對象;代理對象引用一個無效的AppDomain try { //卸載AppDomain之后調(diào)用mbvt方法不會拋出異常 Console.WriteLine("Return object created " + mbvt.ToString()); Console.WriteLine("調(diào)用成功"); } catch (AppDomainUnloadedException) { Console.WriteLine("調(diào)用失敗,AppDomain被卸載了"); } //************************************************************************************************************ //************************************************************ DEMO 3:使用不可封送的類型進行跨AppDomain通信 *** //************************************************************************************************************ ad2 = AppDomain.CreateDomain("AD #2", null, null); mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApplication7.MarshalByRefType"); try { NonMarshalableType nmt = mbrt.MethodArgAndReturn(CallingDomainName);//拋出異常:未標記為可序列化 } catch (SerializationException) { Console.WriteLine("拋出異常:未標記為可序列化"); } Console.ReadKey(); } } //該類型的實例可跨越AppDomain的邊界“按引用封送” public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} ctor running in {1}", GetType(), Thread.GetDomain().FriendlyName); } public void SomeMethod() { Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); } public MarshalByValType MethodWithReturn() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); MarshalByValType t = new MarshalByValType(); return t; } public NonMarshalableType MethodArgAndReturn(string callingDomainName) { //注意:callingDomainName是可序列化的 Console.WriteLine("Calling from '{0}' to '{1}'.", callingDomainName, Thread.GetDomain().FriendlyName); NonMarshalableType t=new NonMarshalableType(); return t; } } //該類的實例可跨越AppDomain的邊界“按值封送” [Serializable] public sealed class MarshalByValType : Object { private DateTime m_creationTime = DateTime.Now;//注意:DateTime是可序列化的 public MarshalByValType() { Console.WriteLine("{0} ctor running in {1}, Created no {2:D}", GetType(), Thread.GetDomain().FriendlyName, m_creationTime); } public override string ToString() { return m_creationTime.ToLongDateString(); } } //該類的實例不能跨AppDomain邊界進行封送 //[Serializable] public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Execute in " + Thread.GetDomain().FriendlyName); } } } 演示1:使用“按引用封送(Marshal-by-Refernce)”AppDomain通信1,AppDomain.CreateDomain三個參數(shù): friendlyName:代表新AppDomain的友好名稱的一個String, securityInfo:一個System.Security.Polict.Evidence,是CLR用于計算AppDomain權限集的證據(jù)。本例為null,造成新的AppDomain從創(chuàng)建它的AppDomain繼承權限集。通常,如果希望圍繞AppDomain中的代碼創(chuàng)建安全邊界,可構造一個System.Security.PermssionSet對象,在其中添加希望的權限對象(實現(xiàn)了IPermission接口的類型實例),將得到的PermssionSet對象引用傳給接受一個PermssionSet的CreateDomain方法 info:一個System.AppDomainSetup,代表CLR為新AppDomain使用的配置設置。同樣,本例為該參數(shù)傳遞為null,是新的AppDomain從創(chuàng)建它的AppDomain繼承配置設置。如果希望對新AppDomain進行特殊配置,可構造一個AppDomainSetup對象,將它的各種屬性(例如置文件的名稱)設為你希望的值,然后將得到的AppDomainSetup對象引用傳給CreateDomain方法 2,AppDomain的CreateInstanceAndUnwrap內(nèi)部實現(xiàn) ①CreateInstanceAndUnwrap方法導致調(diào)用線程從當前AppDomain切換到新的AppDomain ②線程將指定的程序集加載到新AppDomain中,并掃描程序集的類型定義元數(shù)據(jù)表,查找指定類型 ③找到類型后,線程調(diào)用該類型的無參構造器(CreateInstanceAndUnwrap方法一些重載方法允許在調(diào)用類型的構造器時傳遞實參) ④現(xiàn)在線程又切換回默認的AppDomain,時CreateInstanceAndUnwrap能返回對新類型對象的引用 3,“按引用封送”的具體含義 當CreateInstanceAndUnwrapA線它封送的一個對象的類型派生自MarshalByRefObject時,CLR就會跨AppDomain邊界按引用封送 ①源AppDomain想向目標AppDomain發(fā)送或返回對象引用時,CLR會在目標AppDomain的Loader堆中定義一個代理類型,代理類型是用原始類型的元數(shù)據(jù)定義的,所以它看起來和原始類型完全一樣,有完全一樣的實例成員(屬性、事件和方法)。實例字段不會成為(代理)類型的一部分。代理類型確定定義了幾個(自己的)實例字段,但這些字段和原始類型的不一致。相反,這些字段只是指出哪個AppDomain“擁有”真實的對象,以及如何在擁有(對象的)AppDomain中找到真實的對象
②在目標AppDomain中定義好這個代理類型之后,CreateInstanceAndUnwrapA方法就會創(chuàng)建代理類型的實例,初始化它的字段來標識源AppDomain和真實對象,然后將這個代理對象的引用返回給目標AppDomain(調(diào)用該對象的GetType方法,他會向你撒謊) ③應用程序使用代理調(diào)用SomeMethod方法。由于mbrt變量用代理對象,所以會調(diào)用由代理實現(xiàn)的SomeMethod。代理的實現(xiàn)利用代理對象中的信息字段,將調(diào)用線程從默認AppDomain切換至新AppDomain?,F(xiàn)在,該線程的任何行動都在新AppDomain的安全策略和配置設置下運行。線程接著使用代理對象的GCHandle字段查找新AppDomain中的真實對象,并用真實對象調(diào)用真實的SomeMethod方法 ④使用“按引用封送”的語義進行跨AppDomain邊界的對象訪問,會產(chǎn)生一些性能上的開銷。所以一般應該盡量少用這個功能 演示2:使用“按值封送(Marshal-by-Value)”AppDomain通信CLR在目標AppDomain中精確的賦值了源對象。然后MethodWithReturn方法返回對這個副本的引用。源AppDomain中的對象和目標AppDomain中的對象有了獨立的生存期,它們的狀態(tài)也可以獨立地更改 演示2與演示1很相似。和演示1一樣,演示2也創(chuàng)建了新AppDomain。然后調(diào)用CreateInstanceAndUnwrap方法將同一個程序集加載到新建AppDomain中,并在這個新AppDomain中創(chuàng)建MarshalByRefType類型的實例。CLR為這個對象創(chuàng)建代理,mbrt變量(在默認AppDomain中)被初始化成引用這個代理。接著用代理調(diào)用MethodWithReturn方法。這個方法是無參的,將在新AppDomain種執(zhí)行以創(chuàng)建MarshalByValType類型的實例,并將一個對象引用返回給默認AppDomain。 MarshalByValType不從system. MarshalByRefObject派生,所以clr不能定義一個代理類型并創(chuàng)建代理類型的實例:對象不能按引用跨AppDomain邊界進行封送。 但由于MarshalByValType標記了自定義特性[Serializable],所以MethodWithReturn方法能按值封送對象。下面具體描述了將一個對象按值從一個AppDomain封送到另一個AppDomain的含義。 源AppDomain想向目標AppDomain發(fā)送或返回一個對象引用時,clr將對象的實例字段序列化哼一個字節(jié)數(shù)組。字節(jié)數(shù)組從源AppDomain復制到目標AppDomain。然后,clr在模板AppDomain中發(fā)序列化字節(jié)數(shù)組,這會強制clr將定義了“被反序列化的類型”的程序集加載到目標AppDomain中。接著,clr創(chuàng)建類型的實例,并利用字節(jié)數(shù)組中的值初始化對象的字段,使之與源對象中的值相同。換言之,clr在目標AppDomain中精確復制了源對象。然后MethodWithReturn方法返回對這個副本的引用;這樣一來,對象就跨AppDomain的邊界按值封送了。
演示3:使用不可封送的類型跨AppDomain通信由于NonMarshalableType不是從System. MarshalByRefObject中派生的,也沒有用[Serializable]定制特性進行標記,所以不允許MethodArgAndReturn按引用或按值封送對象--對象完全不能跨越AppDomain邊界進行封 演示3與演示1和2相似。都是創(chuàng)建了新AppDomain。然后調(diào)用CreateInstanceAndUnwrap方法將同一個程序集加載到新建AppDomain中,并在這個新AppDomain中創(chuàng)建MarshalByRefType對象,并讓mbrt引用該對象的代理。 然后,我用代理調(diào)用接受一個實參的MethodArgAndReturn方法。同樣地,clr必須保持AppDomain的隔離,所以不能直接將對實參的引用傳給新AppDomain。如果對象的類型派生自MarshalByRefObject,clr會為他創(chuàng)建代理并按引用封送。如果對象的類型用[Serializable]進行了標記,clr會將對象(及其子)序列化成一個字節(jié)數(shù)組,將字節(jié)數(shù)組封送到新AppDomain中,再將字節(jié)數(shù)組反序列化成對象圖,將對象圖的根傳給MethodArgAndReturn方法。 在這個特定的例子中,我跨越AppDomain邊界傳遞一個system.string對象。system.string類型不上從MarshalByRefObject派生的,所以clr不能創(chuàng)建代理。幸好,system.string被標記為[Serializable],所以clr能按值封送它,使代碼能正常工作。注意,對于string對象,clr會采取特殊的優(yōu)化措施??缭紸ppDomain邊界封送一個string對象時,clr只是跨越邊界傳遞對string對象的引用;不會真的生成string對象的副本。之所以能提供這個優(yōu)化,是因為string對象是不可變的;所以,一個AppDomain中的代碼不可能破壞string對象的字段。 在MethodArgAndReturn內(nèi)部,我顯示傳給它的字符串,證明字符串跨越了AppDomain邊界。然后,我創(chuàng)建NonMarshalableType類型的實例,并將對這個對象的引用返回至默認AppDomain。由于NonMarshalableType不是從system.MarshalByRefObject派生的,也沒有用[Serializable]定制特性進行標記,所以不允許MethodArgAndReturn按引用或按值封送對象--—對象完全不能跨域AppDomain邊界進行封送!為了報告這個問題,MethodArgAndReturn在默認AppDomain中拋出一個SerializableException異常。由于我的程序沒有捕捉這個異常,所以程序終止。 卸載appdomainAppDomain很強大的一個地方就是可以卸載它。卸載AppDomain會導致clr卸載AppDomain中的所有程序及,還會釋放AppDomain的loader堆。卸載AppDomain的辦法是調(diào)用AppDomain的靜態(tài)Unload方法。這導致clr執(zhí)行一系列操作來得體地卸載指定AppDomain。 1 clr掛起進程中執(zhí)行過托管代碼的所有線程。 2 CLR檢查所有線程棧,查看哪些線程正在執(zhí)行要卸載的AppDomain中的代碼,或者哪些線性會在某個時候返回至要卸載的AppDomain。任何棧上有要卸載的AppDomain,CLR都會強制對應的線程拋出一個ThreadAbortException(同時恢復線程的執(zhí)行)。這將導致線程展開,并執(zhí)行遇到的所有finally塊以清理資源。如果沒有代碼捕捉ThreadAbortException,它最終會成為未處理的異常,CLR會“吞噬”這個異常,線程會終止,但進程可繼續(xù)執(zhí)行。這是很特別的一點,因為對于其他所有未處理異常,clr都會終止進程。 3 當?shù)?步發(fā)現(xiàn)的所有線程都離開AppDomain后,CLR遍歷堆,為引用了“由于卸載的AppDomain創(chuàng)建的對象”的每個代理對象都設置一個標志(flag)、這些代理對象現(xiàn)在知道他們引用的真實對象已經(jīng)不存在了?,F(xiàn)在,任何代碼在無效的代理對象上調(diào)用方法都會拋出AppDomainUnloadedException異常 4 CLR強制垃圾回收,回收由已卸載的AppDomain創(chuàng)建的任何對象的內(nèi)存。這些對象的Finalze方法被調(diào)用,是對象由機會正確清理他們占用的資源 5 CLR恢復剩余所有線程的執(zhí)行。調(diào)用AppDomain.Unload方法的線程將繼續(xù)運行;對于AppDomain.Unload的調(diào)用是同步進行的 監(jiān)視AppDomainAppDomain的幾條MonitoringEnabled屬性設置為true顯式打開監(jiān)控。打開監(jiān)控后,代碼可查詢AppDomain類提供的以下4個屬性 ①MonitoringSurvivedProcessMemorySize:這個Int64靜態(tài)屬性返回由當前CLR實例控制的所有AppDomain使用的字節(jié)數(shù)。這個數(shù)字值保證在上一次垃圾回收時時準確的 ②MonitoringTotalAllocatedMemorySize:這個Int64實例屬性返回特定AppDomain已分配的字節(jié)數(shù)。這個數(shù)字只保證在上一次垃圾回收時是準確的 ③MonitoringSuvivedMemorySize:這個Int64實例屬性返回特定AppDomain當前正在使用的字節(jié)數(shù)。這個數(shù)字只保證在上一次垃圾回收時是準確的 ④MonitoringTotalProcessorTime:這個TimeSpan實例返回特定AppDomain的CPU占用率 ![]() ![]() using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApplication8 { class Program { static void Main(string[] args) { AppDomainResourceMonitoring(); Console.WriteLine(Environment.TickCount); Console.ReadLine(); } public static void AppDomainResourceMonitoring() { using (new AppDomainMonitorDalte(null)) { //分配在回收時能存活的約10MB var list = new List<object>(); for (int x = 0; x < 1000; x++) { list.Add(new byte[1000]); } //分配在回收時存活不了的約20MB for (int x = 0; x < 2000; x++) { new byte[10000].GetType(); } //保持CPU工作約5秒 var stop = Environment.TickCount + 5000; while (Environment.TickCount < stop) ; } } } public class AppDomainMonitorDalte : IDisposable { private AppDomain m_appdomain; private TimeSpan m_thisADCpu; private Int64 m_thisAdMemoryInUse; private Int64 m_thisADMemoryAllocated; static AppDomainMonitorDalte() { //確定已打開AppDomain監(jiān)視 AppDomain.MonitoringIsEnabled = true; } public AppDomainMonitorDalte(AppDomain ad) { m_appdomain = ad ?? AppDomain.CurrentDomain; m_thisADCpu = m_appdomain.MonitoringTotalProcessorTime; m_thisAdMemoryInUse = m_appdomain.MonitoringSurvivedMemorySize; m_thisADMemoryAllocated = m_appdomain.MonitoringTotalAllocatedMemorySize; } public void Dispose() { GC.Collect(); Console.WriteLine("AppDomain友好名稱={0},CPU={1}ms", m_appdomain.FriendlyName, (m_appdomain.MonitoringTotalProcessorTime - m_thisADCpu).TotalMilliseconds); Console.WriteLine("Allocated {0:N0} bytes of which {1:N0} survied GCs", m_appdomain.MonitoringTotalAllocatedMemorySize - m_thisADMemoryAllocated, m_appdomain.MonitoringSurvivedMemorySize - m_thisAdMemoryInUse); } } }
AppDomain FirstCance異常通知①異常首次拋出時,CLR調(diào)用向拋出異常的AppDomain登記的所有FirstChanceException回調(diào)方法。 ②然后。CLR查找棧上同一個AppDomain中的任何catch塊,有一個catch塊能處理異常,則異常處理完成,將繼續(xù)執(zhí)行 ③如果AppDomain中沒有一個catch塊能處理異常,則CLR沿著棧向上來到調(diào)用AppDomain,再次拋出同一異常對象(序列化和反序列化之后) ④這時感覺就像是拋出一個全新新的異常,CLR調(diào)用當前AppDomain登記的所有FirstChanceException回調(diào)方法 ⑤這個過程會一直執(zhí)行,直到抵達線程棧頂部。如果異常還未處理,則進程終止
宿主如何使用AppDomain前面已討論了宿主以及宿主加載clr的方式。同時還討論了宿主如何告訴clr創(chuàng)建和卸載AppDomain。下面將描述不同應用程序類型如何寄宿clr,以及如何管理AppDomain。
可執(zhí)行應用程序控制臺ui應用程序、nt service應用程序、windows窗體應用程序和windows presentation foundation(wpf)應用程序都是自寄宿(self-hosted,即自己容納clr)的應用程序,它們都有托管exe文件。windows用托管exe文件初始化進程時,會加載墊片。墊片檢查應用程序的程序集(exe文件)中的clr頭信息。頭信息指明了生成和測試應用程序時使用的clr版本。墊片根據(jù)這些信息決定將哪個版本的clr加載到進程中,clr加載并初始化好之后,會再次檢查程序集clr頭,判斷哪個方法是應用程序的入口方法(main)。clr調(diào)用該方法,此時應用程序才真正啟動并運行起來。 代碼運行時會訪問其他類型。引用另一個程序集中的類型時,clr會定位所需的程序集,并將其加載到同一個AppDomain中。應用程序的main方法返回后,windows進程終止(銷毀默認AppDomain和其他所有AppDomain) 注意:要關閉windows進程(包括它所有AppDomain),可調(diào)用system.Environment的靜態(tài)方法Exit。Exit是終止進程最得體的方式,因為它首先調(diào)用托管堆上的所有對象的fimalize方法,再釋放clr容納的所有非托管com對象。最后,exit調(diào)用win32 ExitProcess函數(shù)。
MicrosoftASP.NET和XML Web服務應用程序ASP.NET作為一個ISAPI DLL實現(xiàn),客戶端首次請求有這個dll處理的url時,asp.net會加載clr??蛻舳苏埱笠粋€web應用程序時,ASP.NET判斷這是不是第一次請求。如果是,ASP.NET要求clr為該web應用程序創(chuàng)建新AppDomain;每個web應用程序都根據(jù)虛擬根目錄標識。然后,ASP.NET要求clr將包含應用程序所有公共類型的程序集加載到新AppDomain中,創(chuàng)建該類型的實例,并調(diào)用其中的方法相應客戶端的web請求。如果代碼引用了更多的類型,clr將所需的程序集加載到web應用程序的AppDomain中。 以后,如果客戶端請求已開始運行web應用程序,就不再新建AppDomain了,而是使用現(xiàn)有的AppDomain,創(chuàng)建web應用程序的類型的新實例并開始調(diào)用方法。這些方法已jit編譯成本機代碼,所以后續(xù)客戶端請求的性能會比較出眾。 如果客戶端請求不同的web應用程序,ASP.NET會告訴clr創(chuàng)建新AppDomain。新AppDomain通常在和其他AppDomain一樣的工作進程中創(chuàng)建。這意味著將有大量web應用程序在同一個windows進程中運行,這提升了系統(tǒng)的總體效率。同樣地,每個web應用程序需要的程序集都加載到一個單獨的AppDomain中,以隔離不同web應用程序的代碼和對象。 ASP.NET的一個亮點是允許在不關閉web服務器的前提下動態(tài)更改網(wǎng)站代碼。網(wǎng)站的文件在硬盤上發(fā)生改動時,ASP.NET會檢測到這個情況,并卸載包含舊版本文件的AppDomain(在當前運行的最后一個請求完成之后),并創(chuàng)建一個新AppDomain,向其中加載新版本文件。為確保這個過程順利,ASP.NET使用了AppDomain的一個名為影像復制功能 Mircrosoft Sql ServerMicrosoft sql server是非托管應用程序,它的大部分代碼仍是用非托管c++寫的。sql server 允許開發(fā)人員使用托管代碼創(chuàng)建存儲過程。首次請求數(shù)據(jù)庫運行一個用托管代碼寫的存儲過程時,sql server會加載clr。存儲過程在它們自己的安全AppDomain中運行,這避免了存儲過程對數(shù)據(jù)庫服務器產(chǎn)生負面影響。 高級宿主控制使用托管代碼管理clrsystem.appDomainManager類允許宿主使用托管代碼(而不是非托管代碼)覆蓋clr的默認行為。你唯一要做的就是定義自己的類,讓它從system.appDomainManager派生,重寫想要接手控制的任何虛方法。然后,在專用的程序集中生成類,并將程序集安裝到GAC中。 寫健壯的宿主應用程序托管代碼出現(xiàn)錯誤時,宿主可告訴clr采取什么行動。 1 如果線程執(zhí)行時間過長,clr可終止線程并返回一個響應。 2 clr可卸載appDomain。這會終止線程并返回一個響應。 3 clr可被禁用。這會阻止更多的托管代碼在程序中運行,但仍允許非托管代碼運行。 4 clr 可退出windows進程。首先會終止所有線程,并卸載所有appDomain,使資源清理操作得以執(zhí)行,然后才會終止進程。 宿主如何拿回他的線程宿主應用程序一般都想保持對自己的線程的控制。以一個數(shù)據(jù)庫服務器為例。當一個請求抵達數(shù)據(jù)庫服務器時,線程A獲得請求,并將該請求派發(fā)給線程b以執(zhí)行實際工作。線程B可能要執(zhí)行并不是由數(shù)據(jù)庫服務器的開發(fā)團隊創(chuàng)建和測試的代碼。例如,假定一個請求到達數(shù)據(jù)庫服務器,要執(zhí)行由運行服務器的公司用托管代碼寫的存儲過程。數(shù)據(jù)庫服務器要求存儲過程在自己的AppDomain中運行,這個設計自然是極好的,因為能保障安全,防止存儲過程訪問其AppDomain外部的對象,還能防止代碼訪問不允許訪問的資源。 但是,如果存儲過程的代碼進入死循環(huán)怎么辦?在這種情況下,數(shù)據(jù)庫服務器把它的一個線程派發(fā)給存儲過程代碼,但這個線程一去不復返。這便將數(shù)據(jù)庫服務器置于一個危險的境地;服務器服務器誒未來的行為變得不可預測了。例如,由于線程進入死循環(huán),所以服務器的性能可能變得很糟。服務器是不是應該創(chuàng)建更多的線程?這樣會消耗更多的資源,而且這些線程本身也可能進入死循環(huán)。 為了解決這些問題,宿主可利用線程終止功能。下圖展示了旨在解決落跑(runway)線程的宿主應用程序的典型架構。 1 客戶端向服務器發(fā)送請求 2 服務器線程獲得請求,把它派發(fā)給一個線程池線程來執(zhí)行實際工作。 3 線程池線程獲得客戶端的請求,執(zhí)行由構建并測試宿主應用程序的那個公司寫的可信代碼 4 可信代碼進入一個try塊。從這個try塊中,跨越一個appDomain的邊界進行調(diào)用(通過派生自MarshalByRefObject的一個類型)。AppDomain中包含的是不可信代碼(可能是存儲過程),這些代碼不是由制作宿主應用程序的的啊那個公司生成和測試的。在這個時候,服務器相當于把它的線程的控制權交給了一些不可信的代碼,服務器感到有點緊張了。 5 宿主會記錄接收到客戶端請求的時間。不可信代碼在管理員設定的時間內(nèi)沒有對客戶端做出響應,宿主就會調(diào)用Thread的Abort方法要求clr終止線程池線程,強制它拋出一個ThreadAbortException。 6 這時,線程池線程開始展開(unwind),調(diào)用finally塊,使清理代碼得以執(zhí)行。最后,線程池線程穿越AppDomain邊界返回。由于宿主的存根代碼是從一個try塊中調(diào)用不可信代碼,所以宿主的存根代碼有一個catch塊捕捉ThreadAbortException。 7 為了響應捕捉到的ThreadAbortException異常,宿主調(diào)用Thread的ResetAbort方法。 8 現(xiàn)在,宿主代碼已捕捉到ThreadAbortException異常。因此,宿主可向客戶端返回某種形式的錯誤,允許線程池線程返回線程池,供未來的客戶端請求使用。 澄清一下上述架構中容易被忽視的地方。首先,thread的Abort方法是異步的。調(diào)用Abort方法時,會在設置目標線程的AbortRequested標志后立即返回?!斑\行時”檢測到一個線程要中止時,會嘗試將線程弄到一個安全地點(safe place)。如果“運行時”認為能安全地停止線程正在做的事情,不會造成災難性后果,就說線程在安全地點。如果線程正在執(zhí)行一個托管的阻塞,他就在一個安全地點。如果線程正在執(zhí)行類型的類構造器、catch塊或者finally塊中的代碼、cer中的代碼或者非托管代碼,線程就不在安全地點。 線程到達安全地點后,“運行時”檢測到線程已設置了AbortRequested標志。這導致線程拋出一個ThreadAbortException,如果該異常未被捕捉,異常就會成為未處理的異常,所有掛起的finally塊將執(zhí)行,線程得體地終止。和其他所有異常不同,未處理的ThreadAbortException不會導致應用程序終止?!斑\行時”會悄悄地吞噬這個異常(假裝它沒有發(fā)生),線程將死亡。當應用程序及其剩余的所有線程都將繼續(xù)運行。 在本例中,宿主捕捉ThreadAbortException,允許宿主重新獲取該線程的控制權,并把它歸還到線程池中。但還有一個問題:宿主用什么辦法阻止不可信代碼自己捕獲ThreadAbortException,從而保持宿主對線程的控制呢?答案是CLR以一種非常特殊的方法對待ThreadAbortException。即使代碼捕捉了ThreadAbortException,clr也不允許代碼悄悄地吞噬該異常。換言之,在catch塊的尾部,clr會自動重新拋出ThreadAbortException。 clr 的這個功能又引起另一個問題:如果clr在catch塊的尾部重新拋出了ThreadAbortException異常,宿主如何捕捉它并重新獲取線程的控制權呢?宿主的catch塊中有一個對Thread的ResetAbort方法的調(diào)用。調(diào)用該方法會告訴clr在catch塊的尾部不要重新拋出ThreadAbortException異常。 這又引起了另一個問題:宿主怎么阻止不可信代碼自己捕捉ThreadAbortException并調(diào)用Thread的ResetAbort方法,從而保持宿主對線程的控制呢?答案是Thread的ResetAbort方法要求調(diào)用者被授權了SecurityPermission權限,而且其ControlThread標志已被設置為true。宿主為不可信代碼創(chuàng)建AppDomain時,不會向其授予這個權限,所以不可信代碼不能保持對宿主的線程的控制權。 需要指出的是,這里仍然存在一個潛在的漏洞:當線程從它的ThreadAbortException展開時,不可信代碼可執(zhí)行catch塊和finally塊。在這些塊中,不可信代碼可能進入死循環(huán),阻止宿主重新獲取線程的控制權。宿主應用程序通過設置一個升級策略來修正這個問題。要終止的線程在合理的時間內(nèi)沒有完成,clr可將線程的終止方式升級成“粗魯”的線程終止、“粗魯”的AppDomain卸載、禁用clr甚至殺死整個進程。還要注意,不可信代碼可捕捉ThreadAbortException,并在catch塊中拋出其他種類的一個異常。如果這個其他的異常被捕捉到,clr會在catch塊的尾部自動重新拋出ThreadAbortException異常。 需要指出的是,大多數(shù)不可信的代碼實際并非故意寫成惡意代碼:只是根據(jù)宿主的標準,它們的執(zhí)行時間太長了一點。通常,catch塊和finally塊只包含及少量代碼,這些代碼可以很快地執(zhí)行,不會造成死循環(huán),也不會執(zhí)行耗時很長的任務。所以,宿主為了重新獲取線程的控制權限,一般情況都不會動用升級策略(開始各種各樣的“粗魯”行為)。 順便說一句,thread類實際提供了兩個abort方法:一個無參;另一個獲取一個object參數(shù),允許傳遞任何東西進來。代碼捕捉到ThreadAbortException時,可查詢它的只讀Exception屬性。該屬性返回的就是傳給Abort的對象。這就允許調(diào)用Abort的線程指定了一些額外的信息,供捕捉ThreadAbortException異常的代碼檢查。宿主可利用這個功能讓自己的處理代碼知道它為什么要中止線程。 |
|