雁过请留痕...
代码改变世界

《CLR via C#》笔记——AppDomain(1)

2012-07-17 09:13  xiashengwang  阅读(4292)  评论(0编辑  收藏  举报

一,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),一个完全不能封送的类型。

View Code
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);
        }
    }
}

程序的运行结果如下:

View Code
*** 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特性的类型,所以抛出了异常。

 

未完,下接 《CLR via C#》笔记——AppDomain(2)