《CLR via C#》读书笔记 之 CLR寄宿和AppDomain
第二十三章 CLR寄宿和AppDomain
2013-03-17
22.1 CLR寄宿
22.2 AppDomain
跨越AppDomain边界访问对象
22.3卸载AppDomain
22.1 CLR寄宿
.NET Framework在Microsoft Windows平台跑,意味着它必须用Windows可理解的技术来构建。所以,所有的托管模块和程序集文件必须使用Windows PE文件格式,而且要么是Window EXE文件,要么是DLL文件。
开发CLR时,Microsoft实际上把他实现成在一个DLL中的COM服务器。也就是说,Microsoft为CLR定义了一个标准的COM接口,并为该接口和COM服务器分配了GUID。安装.NET Framework时,代表CLR的COM服务器和其他COM服务器一样在Windows注册表中注册。
任何Windows应用程序都可以寄宿CLR,寄宿CLR步骤:
(1)在Windows检查EXE文件头并决定是创建32位、64位还是WoW64位进程
(2)Windows将x86、x64或IA64版本的MSCorEE.dll加载到进程的地址空间中(参考1.3 加载公共语言运行时)
(3)进程的主线程调用MSCorEE.dll中定义的方法CLRCreateInstance,该方法返回ICLRMetaHost接口。接着调用该接口的方法GetRumtime,该方法返回接口ICLRRuntimeInfo的指针。然后利用该指针和GetIntoface方法获得ICLRRuntimeHost接口,该接口做了一下事情:
- 设置宿主管理器。CLR宿主要做的事:内存管理,程序集加载,安全性,异常处理,以及线程同步。
- 初始化并启动CLR
- 加载程序集并执行其中代码。
- 停止CLR。
22.2 AppDomain
AppDomain是一组程序集的逻辑容器。它的唯一的作用就是进行隔离。请参考进程、线程与应用程序域区别。
具体功能:
(1)两个AppDomain中代码不能直接访问。
(2)AppDomain可以卸载。
(3)AppDomain可以单独保护。
(4)AppDomain可以单独实施配置。
上图中演示了一个Windows进程,其中运行着一个CLR COM服务器。该CLR当前管理者两个AppDomain。每个AppDomain都有自己的Loader堆(参考4.4 运行时相互关系)。除此之外,每个AppDomain都加载了一些程序集。还有一个与进程相同生命周期的“AppDomain中立”提供公用的程序集。
跨越AppDomain边界访问对象
一个AppDomain中的代码可以和另一个AppDomain中的类型和对象通信。但是,只允许通过良好定义的机制访问。下面的代码演示了如何创建一个新的AppDomain,在其中加载一个程序集,然后构造程序集所定义的类型实例。代码演示构造了三种类型的不同行为:
(1)”按引用封送“(Marshal-by-Reference)的类型;
(2)”按值封送“(Marshal-by-value)的类型;
(3)不能封送的类型。
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.Reflection; 5 using System.Runtime.InteropServices; 6 using System.Runtime.Remoting; 7 using System.Threading; 8 9 public sealed class Program 10 { 11 public static void Main() 12 { 13 Marshalling(); 14 } 15 16 private static void Marshalling() 17 { 18 // Get a reference to the AppDomain that that calling thread is executing in 19 AppDomain adCallingThreadDomain = Thread.GetDomain(); 20 21 // Every AppDomain is assigned a friendly string name (helpful for debugging) 22 // Get this AppDomain's friendly string name and display it 23 String callingDomainName = adCallingThreadDomain.FriendlyName; 24 Console.WriteLine("Default AppDomain's friendly name={0}", callingDomainName); 25 26 // Get & display the assembly in our AppDomain that contains the 'Main' method 27 String exeAssembly = Assembly.GetEntryAssembly().FullName; 28 Console.WriteLine("Main assembly={0}", exeAssembly); 29 30 // Define a local variable that can refer to an AppDomain 31 AppDomain ad2 = null; 32 33 // *** DEMO 1: Cross-AppDomain Communication using Marshal-by-Reference 34 Console.WriteLine("{0}Demo #1", Environment.NewLine); 35 // Create new AppDomain (security & configuration match current AppDomain) 36 ad2 = AppDomain.CreateDomain("AD #2", null, null); 37 MarshalByRefType mbrt = null; 38 // Load our assembly into the new AppDomain, construct an object, marshal 39 // it back to our AD (we really get a reference to a proxy) 40 mbrt = (MarshalByRefType) 41 ad2.CreateInstanceAndUnwrap(exeAssembly, "MarshalByRefType"); 42 Console.WriteLine("Type={0}", mbrt.GetType()); // The CLR lies about the type 43 // Prove that we got a reference to a proxy object 44 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt)); 45 // This looks like we're calling a method on MarshalByRefType but, we're not. 46 // We're calling a method on the proxy type. The proxy transitions the thread 47 // to the AppDomain owning the object and calls this method on the real object. 48 mbrt.SomeMethod(); 49 // Unload the new AppDomain 50 AppDomain.Unload(ad2); 51 // mbrt refers to a valid proxy object; the proxy object refers to an invalid AppDomain 52 try 53 { 54 // We're calling a method on the proxy type. The AD is invalid, exception is thrown 55 mbrt.SomeMethod(); 56 Console.WriteLine("Successful call."); 57 } 58 catch (AppDomainUnloadedException) 59 { 60 Console.WriteLine("Failed call."); 61 } 62 63 // *** DEMO 2: Cross-AppDomain Communication using Marshal-by-Value 64 Console.WriteLine("{0}Demo #2", Environment.NewLine); 65 // Create new AppDomain (security & configuration match current AppDomain) 66 ad2 = AppDomain.CreateDomain("AD #2", null, null); 67 // Load our assembly into the new AppDomain, construct an object, marshal 68 // it back to our AD (we really get a reference to a proxy) 69 MarshalByValType mbvt = (MarshalByValType) 70 ad2.CreateInstanceAndUnwrap(exeAssembly, "MarshalByValType"); 71 // Prove that we did NOT get a reference to a proxy object 72 Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt)); 73 // This looks like we're calling a method on MarshalByValType and we are. 74 Console.WriteLine("Returned object created " + mbvt.ToString()); 75 // Unload the new AppDomain 76 AppDomain.Unload(ad2); 77 // mbvt refers to valid object; unloading the AppDomain has no impact. 78 try 79 { 80 // We're calling a method on an object; no exception is thrown 81 Console.WriteLine("Returned object created " + mbvt.ToString()); 82 Console.WriteLine("Successful call."); 83 } 84 catch (AppDomainUnloadedException) 85 { 86 Console.WriteLine("Failed call."); 87 } 88 89 // DEMO 3: Cross-AppDomain Communication using non-marshalable type 90 Console.WriteLine("{0}Demo #3", Environment.NewLine); 91 // Create new AppDomain (security & configuration match current AppDomain) 92 ad2 = AppDomain.CreateDomain("AD #2", null, null); 93 // Load our assembly into the new AppDomain, construct an object, marshal 94 // The object's method returns an non-marshalable object; exception 95 NonMarshalableType nmt = (NonMarshalableType) 96 ad2.CreateInstanceAndUnwrap(exeAssembly, "NonMarshalableType"); 97 // The object's method returns an non-marshalable object; exception 98 } 99 } 100 101 // Instances can be marshaled-by-reference across AppDomain boundaries 102 public sealed class MarshalByRefType : MarshalByRefObject 103 { 104 public MarshalByRefType() 105 { 106 Console.WriteLine("{0} ctor running in {1}", 107 this.GetType().ToString(), Thread.GetDomain().FriendlyName); 108 } 109 110 public void SomeMethod() 111 { 112 Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); 113 } 114 } 115 116 // Instances can be marshaled-by-value across AppDomain boundaries 117 [Serializable] 118 public sealed class MarshalByValType : Object 119 { 120 private DateTime m_creationTime = DateTime.Now; // NOTE: DateTime is [Serializable] 121 122 public MarshalByValType() 123 { 124 Console.WriteLine("{0} ctor running in {1}, Created on {2:D}", 125 this.GetType().ToString(), 126 Thread.GetDomain().FriendlyName, 127 m_creationTime); 128 } 129 130 public override String ToString() 131 { 132 return m_creationTime.ToLongDateString(); 133 } 134 } 135 136 // Instances cannot be marshaled across AppDomain boundaries 137 // [Serializable] 138 public sealed class NonMarshalableType : Object 139 { 140 public NonMarshalableType() 141 { 142 Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName); 143 } 144 }
AppDomain是CLR的功能,Windows对此一无所知。
CreateInstanceAndUnwrap方法导致调用线程从当前AppDomain到新的AppDomain。线程将制定程序集加载到新AppDomain中,并扫描程序集类型定义元数据表,查找指定类型“MarshalByRefType”)。找到类型后,调用它的无参构造函数。然后,线程又范围默认AppDomain,对CreateInstanceAndUnwrap返回的MarshalByRefType对象进行操作。
如何将一个对象从一个AppDomain(源AppDomain,这里指真正创建对象的地方)封送到另一个AppDomain(目标AppDomain,这里指调用CreateInstanceAndUnwrap的地方)?
1. Marshal-by-Reference
CLR会在目标AppDomain的Loader堆中定义一个代理类型。这个代理类型是用原始类型的数据定义的。因此,它看起来和原始类型完全一样;有完全一样的实例成员(属性、事件和方法)。但是,实例字段不会成为(代理)类型的一部分。
2. Marshal-by-Value
CLR将对象字段序列化一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain。然后,CLR在目标AppDomain中反序列化字节数组,这会强制CLR将定义了的“被反序列化的类型”的程序集加载到目标AppDomain中。接着,CLR创建类型的一个实例,并利用字节数组初始化对象的字段,使之与源对象中的值相同。换言之,CLR在目标AppDomain中准确的复制了源对象。
22.3卸载AppDomain
AppDomain.Unload()中执行操作:
(1)CLR挂起进程中执行中执行的托管代码的所有线程;
(2)CLR检查所有线程栈,查看哪些线程正在执行要卸载的那个AppDomain中的代码,或者哪些线程会在某个时刻返回至要卸载的那个AppDomain。在任何一个栈上,如果准备卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException异常(同时恢复线程的执行)。这将导致线程展开(unwind),在展开的过程中执行遇到的所有finally块中的内容,以执行资源清理代码。如果没有代码捕捉ThreadAbortException,它最终会成为一个未处理的异常,CLR会“吞噬”这个异常,线程会终止,但进程可以继续运行。这一点是非常特别的,因为对于其他所有未处理的异常,CLR都会终止进程。
重要提示:如果一个线程当前正在finally块、catch块、类构造器、临界执行区(critical execution region)域或非托管代码中执行,那么CLR不会立即终止该线程。否则,资源清理代码、错误恢复代码、类型初始化代码、关键代码或者其他任何CLR不了解的代码都无法完成,导致应用程序的行为变得无法预测,甚至可能造成安全漏洞。线程在终止时,会等待这些代码块执行完毕。然后当代码块结束时,CLR再强制线程抛出一个ThreadAbortException。
临界区是指线程终止或未处理异常的影响可能不限于当前任务的区域。相反,非临界区中的终止或失败只对出现错误的任务有影响。
(3)当上一步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“已卸载的AppDomain创建的对象”的每一个代理都设置一个标志(flag)。这些代理对象现在知道它们引用的真实对象已经不在了。如果任何代码在一个无效的代理对象上调用一个方法,该方法会抛出一个AppDomainUnloadedException
(4)CLR强制垃圾回收,对现已卸载AppDomain创建的任何对象占用的内存进行回收。这些对象的Finalize方法被调用(如果存在Finalize方法),使对象有机会彻底清理它们占用的资源
(5)CLR恢复剩余所有线程的执行。调用AppDomain.Unload方法的线程将继续执行,对AppDomain.Unload的调用是同步进行的在前面的例子中,所有工作都用一个线程来做。因此,任何时候只要调用AppDomain.Unload都不可能有另一个线程在要卸载的AppDomain中。因此,CLR不必抛出任何ThreadAbortException异常。