使用.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 ");
8 }
9 //访问上下文绑定对象测试
10 public void Sync(ContextBound contextBound)
11 {
12 contextBound.Test("Example call on contextBound ");
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 ");
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} AppDomainID 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} AppDomainID 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} ContextID is:{1}",
27 Thread.CurrentThread.ManagedThreadId, Thread.CurrentContext.ContextID);
28 Console.WriteLine(message);
29 }
30 }
运行结果
本篇总结
进程(Process)、线程(Thread)、应用程序域(AppDomain)、上下文(Context)的关系如图5.0,一个进程内可以包括多个应用程序域,也有包括多个线程,线程也可以穿梭于多个应用程序域当中。但在同一个时刻,线程只会处于一个应用程序域内。线程也能穿梭于多个上下文当中,进行对象的调用。
虽然进程、应用程序域与上下文在平常的开发中并非经常用到,但深入地了解三者的关系,熟悉其操作方式对合理利用系统的资源,提高系统的效率是非常有意义的。
尤其是三者与线程之间的关系尤为重要,特别是在一个多线程系统中,如果不能理清其关系而盲目使用多线程,容易造成资源抢占与死锁之类的错误。