AppDomain
先从传统的Windows进程说起,传统的进程用来描述一组资源和程序运行所必需的内存分配。对于每个被加载到内存的可执行程序,在她的生命周期中操作系统会为之单独且隔离的进程。由于一个进程的失败不会影响其他的进程,使用这种方式,运行库环境将更加稳定。
而一个.NET的应用程序并非直接承载于一个传统的Windows进程中,而是承载在进程的一个逻辑分区中,术语称应用程序域(简称AppDomain)。一个进程可以拥有多个应用程序域,应用程序域的全部目的就是提供隔离性。
一个AppDomain中的代码创建的对象不能由另一个AppDomain中的代码直接访问。AppDomain是一组程序集的逻辑容器,CLR初始化时创建的第一个AppDomain称为"默认AppDomain",这个默认的AppDomain只有在Windows进程终止时才会被销毁。
●AppDomain可以卸载。
●AppDomain可以单独保护。AppDomain在创建后,会应用一个权限集,它决定了在这个AppDomain中运行的程序集的最大权限。
●AppDomain可以单独实施配置。AppDomain在创建后,会关联一组配置设置。这些设置主要影响CLR在AppDomain中加载程序集的方式。这些设置涉及搜索路径,版本绑定重定向,卷影复制及加载器优化。
相比较与传统的:
1.应用程序域是.NET平台操作系统独立性的关键特性。这种逻辑分区将不同操作系统表现加载可执行程序的差异抽象化了。
2.和一个完整的进程相比,应用程序域的CPU和内存占用要小的多。
3.应用程序域为承载的应用程序提供了深度的隔离。一个失败,其他不会失败。
每个AppDomain都有一个Loader堆,每个Loader堆记录了AppDomain自创建以来访问过的类型,每个类型都有一个方法表,方法表的每个记录项都指向Jit编译的本地代码(前提是该方法至少执行过一次)。
有的程序集本来就要由多个AppDomain使用,最典型的例子就是MSCorLib.dll。该程序集包含了System.Object,System.Int32以及其他所有与.Net Framework密不可分的类型。CLR初始化时,该程序集会自动加载,而且所有的AppDomain都共享该程序集的类型。为了减少资源的消耗,MSCorLib.dll程序集以一种"AppDomain中立"的方式加载。也就是说,针对以"AppDomain中立"方式加载的程序集,CLR会为它们维护一个特殊的Loader堆。该Loader堆中所有的类型对象,以及为这些类型定义的方法JIT编译生成的所有本地代码,都会被进程中的所有AppDomain共享。针对此段文字的描述,AppDomain中立则不像是一个AppDomain,而是一个类似于AppDomain的区域(或结构)记录着与AppDomain相似的信息。
跨越AppDomain边界访问对象
能够跨域AppDomain访问的对象,一定要通过两种方式,其一是按值封送,另一个是按引用封送。除此之外非封送类型进行跨越AppDomain访问时会抛出异常。下面例子参考《CLE via C#》。
// 该类的实例可跨越AppDomain的边界"按引用封送" public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} .ctor running in {1}", this.GetType().Name, Thread.GetDomain().FriendlyName); } public void SomeMehtod() { Console.WriteLine("Executing is " + Thread.GetDomain().FriendlyName); } }
先定义一个可按引用封送的类型,再通过以下代码试验
exeAssmeply是当前默认AppDomain的包含Main方法的程序集,即 Assembly.GetEntryAssembly().FullName
执行结果是
*** Demo #1
MarshalByRefType .ctor running in AD #2
Type=AppDomainLib.MarshalByRefType
Is Proxy=True
Executing is AD #2
Fall Call
接下来是按值封送,对可按引用封送的类修改一下,并增加一个可按值封送的类
// 该类的实例可跨越AppDomain的边界"按引用封送" public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} .ctor running in {1}", this.GetType().Name, Thread.GetDomain().FriendlyName); } public void SomeMehtod() { Console.WriteLine("Executing is " + Thread.GetDomain().FriendlyName); } public MarshalByValType MethodWidthReturn() { Console.WriteLine("Executing is " + Thread.GetDomain().FriendlyName); MarshalByValType t = new MarshalByValType(); return t; } } // 该类的实例可跨越AppDomain的边界"按值封送" [Serializable] public sealed class MarshalByValType : Object { private DateTime m_CreateTime = DateTime.Now;//注意DateTime是可序列化的 public MarshalByValType() { Console.WriteLine("{0} ctor running in {1},create on {2}", this.GetType().ToString(), Thread.GetDomain().FriendlyName, m_CreateTime); } public override string ToString() { return m_CreateTime.ToLongDateString(); } }
试验代码如下
这里的按值封送类型是需要通过被另一个AppDomain的实例在非当前AppDomain中构造,故需要属于AD2的对象mbrt进行构造。返回来的结果则是与原本构造的结果完全是两个独立的对象,按值封送实际上是把原有对象进行序列化,传到目标AppDomain时再反序列化,类似于一个深复制的对象,封送后的对象与原有的对象没有任何关联。故运行结果如下
*** Demo #2
MarshalByRefType .ctor running in AD #2
Executing is AD #2
AppDomainLib.MarshalByValType ctor running in AD #2,create on 2012/07/06
16:24:07
Is Porxy=False
Return Object create:2012年月日
Return Object create:2012年月日
最后试验一下不可封送类型
// 该类的实例可跨越AppDomain的边界"按引用封送" public sealed class MarshalByRefType : MarshalByRefObject { public MarshalByRefType() { Console.WriteLine("{0} .ctor running in {1}", this.GetType().Name, Thread.GetDomain().FriendlyName); } public void SomeMehtod() { Console.WriteLine("Executing is " + Thread.GetDomain().FriendlyName); } 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 NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Executing in {0}", Thread.GetDomain().FriendlyName); } }
同样它的构造都是需要靠属于另外的AppDomain的对象在另一个AppDomain中进行构造,试验代码与结果如下
*** Demo #3
MarshalByRefType .ctor running in AD #2
Calling from AppDomainLib.vshost.exe to AD #2
Executing in AD #2
'System.Runtime.Serialization.SerializationException' 例外发生。。。
按引用封送
当CreateInstanceAndUnwrap发现它封送的对象类型派生自MarshalByRefObject,CLR就会跨AppDomain边界按引用封送对象。下面讲述了按引用将一个对象从一个AppDomain(源AppDomain,这里是真正创建对象的地方)封送到另一个AppDomain(目标AppDomain,这里是调用CreateInstanceAndUnwrap的地方)的具体含义。
源AppDomain想向目标AppDomain发送或返回一个对象的引用时,CLR会在目标AppDomain的Loader堆中定义一个代理类型。这个代理类型是用原始类型的元数据生成的。因此,他和原始数据看起来完全一样。有一样的实例成员(事件,属性,方法)。但是实例成员不会成为代理类型的一部分。在这个代理类型中,确实定义了自己的几个实例字段,但这些实例字段和原始数据不一致。相反,这些字段只是用于指出那个AppDomain"拥有"真实的对象,以及如何在拥有对象的AppDomain中找到真实的对象。(在内部,代理对象用一个GCHandle实例引用真实的对象)
这个代理类型在目标AppDomain中定义好之后,CreateInstanceAndUnwrap方法就会创建这个代理类型的实例,初始化它的字段来标识源AppDomain和真实对象,然后将对这个代理对象的引用返回目标AppDomain。CLR一般不允许将一个类型的对象转换成一个不兼容的类型。但在当前这种情况下,CLR允许转型,因为新类型和源类型有相同的实例成员。事实上,用代理对象调用GetType方法,他会向你撒谎,说自己是一个MarshalByRefObject对象。System.Runtime.Remoting.RemotingServices.IsTransparentProxy方法可以用来验证这个对象是一个代理对象。
AppDomain的Unload静态方法会强制CLR卸载指定的AppDomain(包括其中加载的程序集),并强制执行一次垃圾回收,以释放由卸载AppDomain中的代码创建的对象。这时,默认的AppDomain中mbrt变量仍然引用了一个有效的代理对象。但代理对象已不再引用一个有效的AppDomain了(它已经被卸载了)。当试图再次使用代理对象调用SomeMethod方法时,代理的SomeMethod方法会抛出一个AppDomainUnloadedException异常。
由于新创建的AppDomain是没有根的,所以代理引用的原始对象可以被垃圾回收器回收。这当然不理想。但另一方面,如果将原始对象不确定的留在内存中,代理可能不再引用它,而原始对象依然存活,这同样不理想。CLR解决这个问题的办法是使用一个"租约管理器"。一个对象的代理创建好之后,CLR保持对象存活5分钟,如果5分钟之内没有通过代理发出调用,对象就会失效,下次垃圾回收会释放它的对象。每发出一次对对象的调用,"租约管理器"都会续订对象的租期,保证它在接下来的2分钟在内存中保持存活。如果在对象过期之后试图通过一个代理调用它,CLR会抛出一个System.Runtime.Remoting.RemotingException。默认的5分钟和2分钟是可以修改的,你只需要重写MarshalByRefObject的InitializeLifetimeService方法。更多的详情,可以参看SDK文档的"生存期租约"主题。
按值封送
按值封送的类型,需要实现Serializable特性。源AppDomain想向目标AppDomain发送或返回一个对象的引用时,CLR将对象的实例字段序列化成一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain。然后在目标AppDomain中反序列化字节数组,这会强制CLR将定义了"被反序列化的类型"的程序集加载到目标AppDomain中(如果还未加载的话)。接着,CLR创建类型的一个实例,并用字节数组中的值初始化对象的字段,使之与原对象的值相同。换言之,CLR在目标AppDomain中复制了源对象。然后CreateInstanceAndUnwrap返回对这个副本的引用;这样一来,对象就跨AppDomain的边界按值封送了。按值封送不会涉及代理,返回的对象被默认的AppDomain"拥有"。
最后额外提及一下对象上下文,
应用程序域是承载.NET程序集的进程的逻辑分区。与此相似,应用程序域也可以进一步被划分为多个上下文边界(context boundary)。事实上,.NET上下文为单独的应用程序域提供了一种方式,该方式能为一个给定对象建立"特定的家"(specific home)。
使用上下文,CLR可以确保在运行时有特殊需求的对象,可以通过拦截进出上下文的方法调用,得到适当的和一致的处理。这个拦截层允许CLR调整当前的方法调用,以便满足给定上下文的设定要求。比如,如果定义一个C#类型需要自动线程安全(使用【Synchronization】特性),CLR将会在分配期间创建"同步上下文"。
和一个进程定义了默认的应用程序域一样,每一个应用程序域都有一个默认的上下文(context 0)。大多数.NET对象都会被加载到上下文0中。如果CLR判断一个新创建的对象有特殊需求,一个新的上下文边界将会在承载它的应用程序域中被创建。
可以通过Thread.CurrentContext获得上下文,通过context的ContextProperties属性获得描述。同时检查对象是否被跨上下文访问时可以通过RemotingServices.IsObjectOutOfContext(obj)方法进行判断,跨上下文去访问对象也通过代理对象进行访问。如同跨AppDomain中的按引用封送。
但是从属于另一个Context中的对象构造出来的新对象也是从属于默认上下文中。
参考文章