進程、應用程序域與上下文之間的關系
使用.NET建立的可執行程序 *.exe,並沒有直接承載到進程當中,而是承載到應用程序域(AppDomain)當中。應用程序域是.NET引入的一個新概念,它比進程所占用的資源要少,可以被看作是一個輕量級的進程。
在一個進程中可以包含多個應用程序域,一個應用程序域可以裝載一個可執行程序(*.exe)或者多個程序集(*.dll)。這樣可以使應用程序域之間實現深度隔離,即使進程中的某個應用程序域出現錯誤,也不會影響其他應用程序域的正常運作。
當一個程序集同時被多個應用程序域調用時,會出現兩種情況:
第一種情況:CLR分別為不同的應用程序域加載此程序集。
第二種情況:CLR把此程序集加載到所有的應用程序域之外,並實現程序集共享,此情況比較特殊,被稱作為Domain Neutral。
--------------------------------------------------------------------------------------------------------------------------------------
在.NET平臺下,程序集並沒有直接承載在進程中(而傳統的win32程序是直接承載的)。實際上.NET可執行程序承載在進程的一個邏輯分區中,術語稱為應用程序域(也稱AppDomain)。可見,一個進程可以包含多個應用程序域,每一個應用程序域中承載一個.NET可執行程序,這樣的好處如下:
應用程序域是.NET平臺操作系統獨立性的關鍵特性。這種邏輯分區將不同操作系統加載可執行程序的差異抽象化了。
和一個完整的進程相比,應用程序域的CPU和內存占用要小的多。因此CLR加載和卸載應用程序域比起完整的進程來說也快的多。
應用程序域為承載的應用程序提供了深度隔離。如果進程中一個應用程序域失敗了,其他的應用程序域也能保持正常。
AppDomain的主要成員:
CreateDomain():該靜態方法在當前進程中創建一個新的應用程序域。由於CLR能夠根據需要創建應用程序域,所以必須調用這個方法的機會很少。
GetCurrentThreadId():該靜態方法返回當前應用程序域上活動的線程ID。
UnLoad():該靜態方法在進程中卸載指定的應用程序域。
BaseDirectory:獲取基目錄,該目錄用於探測相關的程序集。
CreateInstance():在指定程序集文件中創建指定類型的新實例。
ExecuteAssembly():根據文件名在應用程序域中執行程序集。
GetAssemblies():獲取已加載到此應用程序域中的.NET程序集(基於COM和C的二進制文件除外)。
Load():動態加載程序集到當前應用程序域。
--------------------------------------------------------------------------------------------------------------------------------------
2.1 AppDomain的屬性與方法
在System命名空間當中就存在AppDomain類,用管理應用程序域。下面是AppDomain類的常用屬性:
屬性 | 說明 |
ActivationContext | 獲取當前應用程序域的激活上下文。 |
ApplicationIdentity | 獲得應用程序域中的應用程序標識。 |
BaseDirectory | 獲取基目錄。 |
CurrentDomain | 獲取當前 Thread 的當前應用程序域。 |
Id | 獲得一個整數,該整數唯一標識進程中的應用程序域。 |
RelativeSearchPath | 獲取相對於基目錄的路徑,在此程序集沖突解決程序應探測專用程序集。 |
SetupInformation | 獲取此實例的應用程序域配置信息。 |
表2.0
AppDomain類中有多個方法,可以用於創建一個新的應用程序域,或者執行應用程序域中的應用程序。
方法 | 說明 |
CreateDomain | 創建新的應用程序域。 |
CreateInstance | 創建在指定程序集中定義的指定類型的新實例。 |
CreateInstanceFrom | 創建在指定程序集文件中定義的指定類型的新實例。 |
DoCallBack | 在另一個應用程序域中執行代碼,該應用程序域由指定的委托標識。 |
ExecuteAssembly | 執行指定文件中包含的程序集。 |
ExecuteAssemblyByName | 執行程序集。 |
GetAssemblies | 獲取已加載到此應用程序域的執行上下文中的程序集。 |
GetCurrentThreadId | 獲取當前線程標識符。 |
GetData | 為指定名稱獲取存儲在當前應用程序域中的值。 |
IsDefaultAppDomain | 返回一個值,指示應用程序域是否是進程的默認應用程序域。 |
SetData | 為應用程序域屬性分配值。 |
Load | 將 Assembly 加載到此應用程序域中。 |
Unload | 卸載指定的應用程序域。 |
表2.1
AppDomain類中有多個事件,用於管理應用程序域生命周期中的不同部分。
事件 | 說明 |
AssemblyLoad | 在加載程序集時發生。 |
AssemblyResolve | 在對程序集的解析失敗時發生。 |
DomainUnload | 在即將卸載 AppDomain 時發生。 |
ProcessExit | 當默認應用程序域的父進程存在時發生。 |
ReflectionOnlyAssemblyResolve | 當程序集的解析在只反射上下文中失敗時發生。 |
ResourceResolve | 當資源解析因資源不是程序集中的有效鏈接資源或嵌入資源而失敗時發生。 |
TypeResolve | 在對類型的解析失敗時發生。 |
UnhandledException | 當某個異常未被捕獲時出現。 |
表2.2
三、深入了解.NET上下文
3.1 .NET上下文的概念
應用程序域是進程中承載程序集的邏輯分區,在應用程序域當中,存在更細粒度的用於承載.NET對象的實體,那就.NET上下文Context。
所有的.NET對象都存在於上下文當中,每個AppDomain當中至少存在一個默認上下文(context 0)。
一般不需要指定特定上下文的對象被稱為上下文靈活對象(context-agile),建立此對象不需要特定的操作,只需要由CLR自行管理,一般這些對象都會被建立在默認上下文當中。
圖3.0
3.2 透明代理
在上下文的接口當中存在著一個消息接收器負責檢測攔截和處理信息,當對象是MarshalByRefObject的子類的時候,CLR將會建立透明代理,實現對象與消息之間的轉換。
應用程序域是CLR中資源的邊界,一般情況下,應用程序域中的對象不能被外界的對象所訪問。而MarshalByRefObject 的功能就是允許在支持遠程處理的應用程序中跨應用程序域邊界訪問對象,在使用.NET Remoting遠程對象開發時經常使用到的一個父類。
此文章針對的是進程與應用程序域的作用,關於MarshalByRefObject的使用已經超越了本文的範圍,關於.NET Remoting 遠程對象開發可參考:“回顧.NET Remoting分布式開發”。
3.3 上下文綁定
當系統需要對象使用消息接收器機制的時候,即可使用ContextBoundObject類。ContextBoundObject繼承了MarshalByRefObject類,保證了它的子類都會通過透明代理被訪問。
在第一節介紹過:一般類所建立的對象為上下文靈活對象(context-agile),它們都由CLR自動管理,可存在於任意的上下文當中。而 ContextBoundObject 的子類所建立的對象只能在建立它的對應上下文中正常運行,此狀態被稱為上下文綁定。其他對象想要訪問ContextBoundObject 的子類對象時,都只能通過代透明理來操作。
下面的例子,是上下文綁定對象與上下文靈活對象的一個對比。Example 是一個普通類,它的對象會運行在默認上下文當中。而ContextBound類繼承了ContextBoundObject,它的對象是一個上下文綁定對象。ContextBound還有一個Synchronization特性,此特性會保證ContextBound對象被加載到一個線程安全的上下文當中運行。另外,Context類存在ContextProperties屬性,通過此屬性可以獲取該上下文的已有信息。
1 class Program
2 {
3 public class Example
4 {
5 public void Test()
6 {
7 ContextMessage("Example Test\n");
8 }
9 //訪問上下文綁定對象測試
10 public void Sync(ContextBound contextBound)
11 {
12 contextBound.Test("Example call on contextBound\n");
13 }
14 }
15
16 [Synchronization]
17 public class ContextBound:ContextBoundObject
18 {
19 public void Test(string message)
20 {
21 ContextMessage(message);
22 }
23 }
24
25 static void Main(string[] args)
26 {
27 Example example = new Example();
28 example.Test();
29 ContextBound contextBound = new ContextBound();
30 contextBound.Test("ContentBound Test\n");
31 example.Sync(contextBound);
32 Console.ReadKey();
33 }
34
35 //顯示上下文信息
36 public static void ContextMessage(string data)
37 {
38 Context context = Thread.CurrentContext;
39 Console.WriteLine(string.Format("{0}ContextId is {1}", data, context.ContextID));
40 foreach (var prop in context.ContextProperties)
41 Console.WriteLine(prop.Name);
42 Console.WriteLine();
43 }
44 }
運行結果
由運行結果可以發現,example對象一般只會工作於默認上下文context 0 當中,而contextBound則會工作於線程安全的上下文 context 1當中。當example需要調用contextBound對象時,就會通過透明代理把消息直接傳遞到context 1中。
回到目錄
四、進程、應用程序域、線程的相互關系
4.1 跨AppDomain運行代碼
在應用程序域之間的數據是相對獨立的,當需要在其他AppDomain當中執行當前AppDomain中的程序集代碼時,可以使用CrossAppDomainDelegate委托。把CrossAppDomainDelegate委托綁定方法以後,通過AppDomain的DoCallBack方法即可執行委托。
1 static void Main(string[] args)
2 {
3 Console.WriteLine("CurrentAppDomain start!");
4 //建立新的應用程序域對象
5 AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain");
6 //綁定CrossAppDomainDelegate的委托方法
7 CrossAppDomainDelegate crossAppDomainDelegate=new CrossAppDomainDelegate(MyCallBack);
8 //綁定DomainUnload的事件處理方法
9 newAppDomain.DomainUnload += (obj, e) =>
10 {
11 Console.WriteLine("NewAppDomain unload!");
12 };
13 //調用委托
14 newAppDomain.DoCallBack(crossAppDomainDelegate);
15 AppDomain.Unload(newAppDomain) ;
16 Console.ReadKey();
17 }
18
19 static public void MyCallBack()
20 {
21 string name = AppDomain.CurrentDomain.FriendlyName;
22 for(int n=0;n<4;n++)
23 Console.WriteLine(string.Format( " Do work in {0}........" , name));
24 }
運行結果
4.2 跨AppDomain的線程
線程存在於進程當中,它在不同的時刻可以運行於多個不同的AppDomain當中。它是進程中的基本執行單元,在進程入口執行的第一個線程被視為這個進程的主線程。在.NET應用程序中,都是以Main()方法作為入口的,當調用此方法時 系統就會自動創建一個主線程。線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當前所執行線程的狀態,調用棧主要用於維護線程所調用到的內存與數據,TLS主要用於存放線程的狀態信息。
關於線程的介紹,可參考 “C#綜合揭秘——細說多線程(上)”、“C#綜合揭秘——細說多線程(下)”
下面的例子將介紹一下如何跨AppDomain使用線程,首先建立一個ConsoleApplication項目,在執行時輸入當前線程及應用程序域的信息,最後生成Example.exe的可執行程序。
1 static void Main(string[] args)
2 {
3 var message = string.Format(" CurrentThreadID is:{0}\tAppDomainID is:{1}",
4 Thread.CurrentThread.ManagedThreadId, AppDomain.CurrentDomain.Id);
5 Console.WriteLine(message);
6 Console.Read();
7 }
然後再新建一個ConsoleApplication項目,在此項目中新一個AppDomain對象,在新的AppDomain中通過ExecuteAssembly方法執行Example.exe程序。
1 static void Main(string[] args)
2 {
3 //當前應用程序域信息
4 Console.WriteLine("CurrentAppDomain start!");
5 ShowMessage();
6
7 //建立新的應用程序域對象
8 AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain");
9 //在新的應用程序域中執行Example.exe
10 newAppDomain.ExecuteAssembly("Example.exe");
11
12 AppDomain.Unload(newAppDomain);
13 Console.ReadKey();
14 }
15
16 public static void ShowMessage()
17 {
18 var message = string.Format(" CurrentThreadID is:{0}\tAppDomainID is:{1}",
19 Thread.CurrentThread.ManagedThreadId, AppDomain.CurrentDomain.Id);
20 Console.WriteLine(message);
21 }
運行結果
可見,ID等於9的線程在不同時間內分別運行於AppDomain 1與AppDomain 2當中。
4.3 跨上下文的線程
線程既然能夠跨越AppDomain的邊界,當然也能跨越不同的上下文。
下面這個例子中,線程將同時運行在默認上下文與提供安全線程的上下文中。
1 class Program
2 {
3 [Synchronization]
4 public class ContextBound : ContextBoundObject
5 {
6 public void Test()
7 {
8 ShowMessage();
9 }
10 }
11
12 static void Main(string[] args)
13 {
14 //當前應用程序域信息
15 Console.WriteLine("CurrentAppDomain start!");
16 ShowMessage();
17
18 //在上下文綁定對象中運行線程
19 ContextBound contextBound = new ContextBound();
20 contextBound.Test();
21 Console.ReadKey();
22 }
23
24 public static void ShowMessage()
25 {
26 var message = string.Format(" CurrentThreadID is:{0}\tContextID is:{1}",
27 Thread.CurrentThread.ManagedThreadId, Thread.CurrentContext.ContextID);
28 Console.WriteLine(message);
29 }
30 }
運行結果
本篇總結
進程(Process)、線程(Thread)、應用程序域(AppDomain)、上下文(Context)的關系如圖5.0,一個進程內可以包括多個應用程序域,也有包括多個線程,線程也可以穿梭於多個應用程序域當中。但在同一個時刻,線程只會處於一個應用程序域內。線程也能穿梭於多個上下文當中,進行對象的調用。
雖然進程、應用程序域與上下文在平常的開發中並非經常用到,但深入地了解三者的關系,熟悉其操作方式對合理利用系統的資源,提高系統的效率是非常有意義的。
尤其是三者與線程之間的關系尤為重要,特別是在一個多線程系統中,如果不能理清其關系而盲目使用多線程,容易造成資源搶占與死鎖之類的錯誤。
進程、應用程序域與上下文之間的關系