一,Appdomain概述
CLR COM服务器初始化时,会创建一个AppDomain。AppDomain是一组程序集的逻辑容器。宿主可以通过CLR创建额外的AppDomain。AppDomain的唯一作用就是隔离。下面是它的具体功能。
●一个AppDomain中的代码创建的对象不能由另一个AppDomain中的代码直接访问。
●AppDomain可以卸载。
●AppDomain可以单独保护。AppDomain在创建后,会应用一个权限集,它决定了在这个AppDomain中运行的程序集的最大权限。
●AppDomain可以单独实施配置。AppDomain在创建后,会关联一组配置设置。这些设置主要影响CLR在AppDomain中加载程序集的方式。这些设置涉及搜索路径,版本绑定重定向,卷影复制及加载器优化。
二,AppDomain的进程模型
一个Windows进程中的AppDomain数量没有硬性限制。每个AppDomain都有一个Loader堆,每个Loader堆记录了AppDomain自创建以来访问过的类型,每个类型都有一个方法表,方法表的每个记录项都指向Jit编译的本地代码(前提是该方法至少执行过一次)。我们来看看,在Windows进程中加载AppDomain后的模型图。
如图所示,AppDomain #1中加载了MyApp.exe,TypeLib.dll,System.dll三个程序集;AppDomain #2中加载了WindABC.dll,System.dll两个程序集;System.dll程序集被加载到了两个AppDomain中。如果这两个AppDomain都使用了来自System.dll的一个类型,那么在两个AppDomain的Loader堆中,都会为同一类型分配一个类型对象;类型对象的内存不会为两个AppDomain共享。另外,一个AppDomain中的代码调用一个类型定义的方法时,方法的IL代码会进行JIT编译,生成本地(native)代码将与每个AppDomain相关联;方法的代码不由调用它的所有AppDomain共享。
不共享类型对象的内存和本地代码,这当然是一种浪费。但是,AppDomain的全部目的就是提供隔离性;CLR要求在卸载某个AppDomain并释放它的所有资源的同时,不会对其他的AppDomain产生负面影响。通过复制CLR的数据结构而不共用,就可以保证这一点。除此之外,还能保证多个AppDomain使用的一个类型在AppDomain中都有一组静态字段。
有的程序集本来就要由多个AppDomain使用,最典型的例子就是MSCorLib.dll。该程序集包含了System.Object,System.Int32以及其他所有与.Net Framework密不可分的类型。CLR初始化时,该程序集会自动加载,而且所有的AppDomain都共享该程序集的类型。为了减少资源的消耗,MSCorLib.dll程序集以一种“AppDomain中立”的方式加载。也就是说,针对以“AppDomain中立”方式加载的程序集,CLR会为它们维护一个特殊的Loader堆。该Loader堆中所有的类型对象,以及为这些类型定义的方法JIT编译生成的所有本地代码,都会被进程中的所有AppDomain共享。遗憾的是,共享这些资源带来的收益并不是没有代价的。这个代价就是,以“AppDomain中立”的方式加载的所有程序集永远不能卸载。为了回收他们的资源,唯一的办法就是终止Windows进程,让Windows去回收资源。
三,跨越AppDomain边界访问对象
一个AppDomain中的代码可以和另一个AppDomain中的类型和对象通信。但是,只允许通过良好定义的机制访问这些类型和对象。下面的代码演示了构造以下三种类型时不同的行为:一个“按引用封送”(Marshal-by-Reference),一个“按值封送”(Marshal-by-Value),一个完全不能封送的类型。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Reflection; using System.Runtime.Remoting; namespace AppDomainLib { public class Marshal { private static void Marshaling() { //获取AppDomain的一个引用(“调用线程”在该AppDomain中执行) AppDomain adCallingThreadDomain = Thread.GetDomain(); //每个AppDomain都有一个友好字符串名称,获取这个名称并显示 string callingDomainName = adCallingThreadDomain.FriendlyName; Console.WriteLine("Defalut AppDomain's friendly name={0}", callingDomainName); //获取&显示我们的AppDomain中包含“Main”方法的程序集 string exeAssembly = Assembly.GetEntryAssembly().FullName; Console.WriteLine("Main assembly={0}", exeAssembly); //定义一个局部变量引用一个AppDomain AppDomain ad2 = null; //*** Demo 1,使用Marshal-by-Reference进行跨AppDomain通信 *** Console.WriteLine("{0}*** Demo #1", Environment.NewLine); //新建一个AppDomain,安全性和配置匹配与当前的AppDomain ad2 = AppDomain.CreateDomain("AD #2", null, null); MarshalByRefType mbrt = null; //将我们的程序集加载到AppDomain中,构造一个对象,把它封送会我们的AppDomain //实际上得到的是一个代理引用 mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "AppDomainLib.MarshalByRefType"); Console.WriteLine("Type={0}", mbrt.GetType());//这里CLR在类型上撒谎了,得到Type=AppDomainLib.MashalByRefType,其实并不是这样 //证明得到的是一个代理的引用 Console.WriteLine("Is Proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); //看起来像是在MashalByRefType上调用了一个方法,实在不然 //我们是在代理类型上调用了一个方法,代理使线程切换至拥有对象 //的那个AppDomain mbrt.SomeMehtod(); //卸载新的AppDomain AppDomain.Unload(ad2); //mbrt引用了一个无效的代理对象,代理对象引用了一个无效的AppDomain try { mbrt.SomeMehtod(); } catch (AppDomainUnloadedException) { Console.WriteLine("Fall Call"); } //*** Demo 2,使用Marshal-by-Value进行跨AppDomain通信 *** Console.WriteLine("{0}*** Demo #2", Environment.NewLine); //新建一个AppDomain,安全性和配置匹配与当前的AppDomain ad2 = AppDomain.CreateDomain("AD #2", null, null); //将我们的程序集加载到AppDomain中,构造一个对象,把它封送会我们的AppDomain //实际上得到的是一个代理引用 mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "AppDomainLib.MarshalByRefType"); //对象的方法返回所返回对象的一个副本 //返回的对象是按值(而非引用)封送 MarshalByValType mbv = mbrt.MethodWidthReturn(); //证明我们得到的不是对一个代理对象的引用 Console.WriteLine("Is Porxy={0}", RemotingServices.IsTransparentProxy(mbv)); //看起来像是在MarshalByValType上调用方法,事实确实如此 Console.WriteLine("Return Object create:{0}", mbv.ToString()); //卸载AppDomain AppDomain.Unload(ad2); //mbv引用有效的对象,卸载AppDomain没有影响 try { //我们是在对象上调用一个方法,所有不会抛出异常 Console.WriteLine("Return Object create:{0}", mbv.ToString()); } catch (AppDomainUnloadedException) { Console.WriteLine("Fail Call"); } //*** Demo 3 使用不可封送的类型进行AppDomain通信 **** Console.WriteLine("{0}*** Demo #3", Environment.NewLine); //新建一个AppDomain,安全性和配置匹配与当前的AppDomain ad2 = AppDomain.CreateDomain("AD #2", null, null); //将我们的程序集加载到AppDomain中,构造一个对象,把它封送会我们的AppDomain //实际上得到的是一个代理引用 mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "AppDomainLib.MarshalByRefType"); //对象的方法返回一个不可封送的对象,抛出异常 NonMarshalableType nmt= mbrt.MethodArgAndReturn(callingDomainName); //这里的代码永远执行不到。。。 } public static void Main() { Marshaling(); } } // 该类的实例可跨越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; } 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_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进行封送 //[Serializable] public sealed class NonMarshalableType : Object { public NonMarshalableType() { Console.WriteLine("Executing in {0}", Thread.GetDomain().FriendlyName); } } }
程序的运行结果如下:
*** Demo #1 MarshalByRefType .ctor running in AD #2 Type=AppDomainLib.MarshalByRefType Is Proxy=True Executing is AD #2 Fall Call *** 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年月日 *** Demo #3 MarshalByRefType .ctor running in AD #2 Calling from AppDomainLib.vshost.exe to AD #2 Executing in AD #2 'System.Runtime.Serialization.SerializationException' 例外发生。。。
CLR不允许一个AppDomain中的变量引用另一个AppDomain中创建的对象。如果CreateInstanceAndUnwrap函数只返回对象的引用,AppDomain提供的隔离性就会被打破,而隔离是AppDomain的全部目的!因此在CreateInstanceAndUnwrap返回对象之前,它要执行一些额外的逻辑。
CreateInstanceAndUnwrap导致调用线程从当前AppDomain转至新的AppDomain,它们用的是同一个线程,所有从这一点也可以看出,线程是可以跨越AppDomain的。并且,跨AppDoman边界的方法调用是同步执行的。如果希望多个AppDomain中的代码并非执行,应创建额外的线程。
3.1 “按引用封送”
当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文档的“生存期租约”主题。
3.2“按值封送”
按值封送的类型,需要实现Serializable特性。源AppDomain想向目标AppDomain发送或返回一个对象的引用时,CLR将对象的实例字段序列化成一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain。然后在目标AppDomain中反序列化字节数组,这会强制CLR将定义了“被反序列化的类型”的程序集加载到目标AppDomain中(如果还未加载的话)。接着,CLR创建类型的一个实例,并用字节数组中的值初始化对象的字段,使之与原对象的值相同。换言之,CLR在目标AppDomain中复制了源对象。然后CreateInstanceAndUnwrap返回对这个副本的引用;这样一来,对象就跨AppDomain的边界按值封送了。按值封送不会涉及代理,返回的对象被默认的AppDomain“拥有”。
3.3 使用不可封送的类型跨越AppDomain
注意这个例子,调用方法是传给了源AppDomain一个String类型的callingDomainName参数,因为String类有Serializable特性,所以能正常传入。函数返回时,返回了一个没有Serializable特性的类型,所以抛出了异常。