细说进程、应用程序域与上下文之间的关系
一、进程的概念与作用
进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法直接访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。
?
?
1.1 Process 的属性与方法
在 System.Diagnostics 命名空间当中存在Process类,专门用于管理进程的开始、结束,访问进程中的模块,获取进程中的线程,设定进程的优先级别等。
表1.0 显示了Process类的常用属性:
?表1.0
除了上述属性,Process类也定义了下列经常使用的方法:
?表1.1
Process类的详细信息可以参考 http://msdn.microsoft.com/zh-cn/library/system.diagnostics.process.aspx
下面将举例介绍一下Process的使用方式
?
?
1.2 建立与销毁进程
利用 Start 与Kill 方法可以简单建立或者销毁进程,下面例子就是利用 Start 方法启动记事本的进程,并打开File.txt文件。2秒钟以后,再使用 Kill 方法销毁进程,并关闭记事本。
?
如果已知进程的Id,就可以通过 GetProcessById 方法获取对应的进程。
?
回到目录
二、应用程序域
使用.NET建立的可执行程序 *.exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中。应用程序域是.NET引入的一个新概念,它比进程所占用的资源要少,可以被看作是一个轻量级的进程。
在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序(*.exe)或者多个程序集(*.dll)。这样可以使应用程序域之间实现深度隔离,即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作。
当一个程序集同时被多个应用程序域调用时,会出现两种情况:
第一种情况:CLR分别为不同的应用程序域加载此程序集。
第二种情况:CLR把此程序集加载到所有的应用程序域之外,并实现程序集共享,此情况比较特殊,被称作为Domain Neutral。
?
?
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
下面将举例详细介绍一下AppDomain的使用方式
?
?
2.2 在AppDomain中加载程序集
由表2.1中可以看到,通过CreateDomain方法可以建立一个新的应用程序域。
下面的例子将使用CreateDomain建立一个应用程序域,并使用Load方法加载程序集Model.dll。最后使用GetAssemblies方法,列举此应用程序域中的所有程序集。
注意:当加载程序集后,就无法把它从AppDomain中卸载,只能把整个AppDomain卸载。
当需要在AppDomain加载可执行程序时,可以使用ExecuteAssembly方法。
AppDomain.ExecuteAssembly("Example.exe");
?
?
2.3 卸载AppDomain
通过Unload可以卸载AppDomain,在AppDomain卸载时将会触发DomainUnload事件。
下面的例子中,将会使用CreateDomain建立一个名为NewAppDomain的应用程序域。然后建立AssemblyLoad的事件处理方法,在程序集加载时显示程序集的信息。最后建立DomainUnload事件处理方法,在AppDomain卸载时显示卸载信息。
?
?
2.4 在AppDomain中建立程序集中指定类的对象
使用CreateInstance方法,能建立程序集中指定类的对像。但使用此方法将返回一个ObjectHandle对象,若要将此值转化为原类型,可调用Unwrap方法。
下面例子会建立Model.dll程序集中的Model.Person对象。
图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属性,通过此属性可以获取该上下文的已有信息。
由运行结果可以发现,example对象一般只会工作于默认上下文context 0 当中,而contextBound则会工作于线程安全的上下文 context 1当中。当example需要调用contextBound对象时,就会通过透明代理把消息直接传递到context 1中。
?
?
回到目录
四、进程、应用程序域、线程的相互关系
4.1 跨AppDomain运行代码
在应用程序域之间的数据是相对独立的,当需要在其他AppDomain当中执行当前AppDomain中的程序集代码时,可以使用CrossAppDomainDelegate委托。把CrossAppDomainDelegate委托绑定方法以后,通过AppDomain的DoCallBack方法即可执行委托。
?
4.2 跨AppDomain的线程
线程存在于进程当中,它在不同的时刻可以运行于多个不同的AppDomain当中。它是进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时 系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
关于线程的介绍,可参考 “C#综合揭秘——细说多线程(上)”、“C#综合揭秘——细说多线程(下)”
下面的例子将介绍一下如何跨AppDomain使用线程,首先建立一个ConsoleApplication项目,在执行时输入当前线程及应用程序域的信息,最后生成Example.exe的可执行程序。
可见,ID等于9的线程在不同时间内分别运行于AppDomain 1与AppDomain 2当中。
?
4.3 跨上下文的线程
线程既然能够跨越AppDomain的边界,当然也能跨越不同的上下文。
下面这个例子中,线程将同时运行在默认上下文与提供安全线程的上下文中。
?
本篇总结
进程(Process)、线程(Thread)、应用程序域(AppDomain)、上下文(Context)的关系如图5.0,一个进程内可以包括多个应用程序域,也有包括多个线程,线程也可以穿梭于多个应用程序域当中。但在同一个时刻,线程只会处于一个应用程序域内。线程也能穿梭于多个上下文当中,进行对象的调用。
虽然进程、应用程序域与上下文在平常的开发中并非经常用到,但深入地了解三者的关系,熟悉其操作方式对合理利用系统的资源,提高系统的效率是非常有意义的。
尤其是三者与线程之间的关系尤为重要,特别是在一个多线程系统中,如果不能理清其关系而盲目使用多线程,容易造成资源抢占与死锁之类的错误。
?图5.0
希望本篇文章对相关的开发人员有所帮助。