AppDomain

作者:追风剑情 发布于:2020-10-29 9:19 分类:C#

一、AppDomain

CLR COM服务器初始化时会创建一个AppDomain。AppDomain是一组程序集的逻辑容器。CLR初始化时创建的第一个AppDomain称为“默认AppDomain”,这个默认的AppDomain只有在Windows进程终止时才会被销毁。

除了默认AppDomain,正在使用非托管COM接口方法或托管类型方法的宿主还可以要求CLR创建额外的AppDomain。AppDomain是为了提供隔离而设计的。下面总结了AppDomain的具体功能:

  • 一个AppDomain的代码不能直接访问另一个AppDomain的代码创建的对象
  • 一个AppDomain中的代码创建了一个对象后,该对象便被该AppDomain“拥有”。换言之,它的生存期不能超过创建它的代码所在的AppDomain。一个AppDomain中的代码要访问另一个AppDomain中的对象,只能使用“按引用封送”(marshal-by-reference)或者"按值封送"(marshal-by-value)的语义。这就强制建立了清晰的分隔和边界,因为一个AppDomain中的代码不能直接引用另一个AppDomain中的代码创建的对象。这种隔离使AppDomain能很容易地从进程中卸载,不会影响其他AppDomain正在运行的代码。
  • AppDomain可以卸载
  • CLR不支持从AppDomain中卸载特定的程序集。但可以告诉CLR卸载一个AppDomain,从而卸载该AppDomain当前包含的所有程序集。
  • AppDomain可以单独保护
  • AppDomain创建后会应用一个权限集,它决定了向这个AppDomain中运行的程序集授予的最大权限。正是由于存在这些权限,所以当宿主加载一些代码后,可以保证这些代码不会破坏(或读取)宿主本身使用的一些重要数据结构。
  • AppDomain可以单独配置
  • AppDomain创建后会关联一组配置设置。这些设置主要影响CLR在AppDomain中加载程序集的方式。涉及搜索路径、版本绑定重定向、卷影复制以及加载器优化。
重要提示  Windows的一个重要特色是让每个应用程序都在自己的进程地址空间中运行。这就保证了一个应用程序的代码不能访问另一个应用程序使用的代码或数据。进程隔离可防范安全漏洞、数据破坏和其他不可预测的行为,确保了Windows系统以及在它上面运行的应用程序的健壮性。遗憾的是,在Windows中创建进程的开销很大。Win32 CreateProcess函数的速度很慢,而且Windows需要大量内存来虚拟化进程的地址空间。但是,如果应用程序完全由托管代码构成(这些代码的安全性可以验证),同时这些代码没有调用非托管代码,那么在一个Windows进程中运行多个托管应用程序是没有问题的。AppDomain提供了保护、配置和终止其中每一个应用程序所需的隔离。

下图演示了一个Windows进程, 其中运行着一个CLR COM服务器。该CLR当前管理着两个AppDomain(虽然在一个Windows进程中可以运行的AppDomain数量没有硬性限制)。每个AppDomain都有自己的Loader堆,每个Loader堆都记录了自AppDomain创建以来已访问过哪些类型。Loader堆中的每个类型对象都有一个方法表,方法表中的每个记录项都指向JIT编译的本机代码(前提是方法至少执行过一次)。除此之外,每个AppDomain都加载了一些程序集。AppDomain #1(默认AppDomain)有三个程序集:MyApp.exe,TypeLib.dll和System.dll。AppDomain #2有两个程序集:Wintellect.dll和System.dll。
Windows进程.png
如上图所示,两个AppDomain都加载了System.dll程序集。如果这两个AppDomain都使用了来自System.dll的一个类型,那么两个AppDomain的Loader堆会为相同的类型分别分配一个类型对象;类型对象的内存不会由两个AppDomain共享。另外,一个AppDomain中的代码调用一个类型定义的方法时,方法的IL代码会进行JIT编译,生成的本机代码单独与每个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中的类型和对象通信,但只能通过良好定义的机制进行。下面的示例程序演示了如何创建新AppDomain,在其中加载程序集并构造该程序集定义的类型的实例。代码演示了以下三种类型在构造时的不同行为:“按引用封送”(Marshal-by-Reference)类型,“按值封送”(Marshal-by-Value)类型,以及完全不能封送的类型。代码还演示了创建它们的AppDomain卸载时这些对象的不同行为。

using System;
using System.Security.Permissions;
using System.Runtime.Serialization;
using System.Reflection;
using System.Threading;
using System.Runtime.Remoting;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
            Marshalling();
            Console.Read();
        }

        private static void Marshalling()
        {
            // 获取AppDomain引用("调用线程"当前正在该AppDomain中执行)
            AppDomain adCallingThreadDomain = Thread.GetDomain();

            // 每个AppDomain都分配了友好字符串名称(以便调式)
            // 获取这个AppDomain的友好字符串名称并显示它
            String callingDomainName = adCallingThreadDomain.FriendlyName;
            Console.WriteLine("Default 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("AppDomain#2", null, null);
            MarshalByRefType mbrt = null;

            // 将我们的程序集加载到新AppDomain中,构造一个对象,把它
            // 封送回我们的AppDomain(实际得到对一个代理的引用)
            mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApp9.MarshalByRefType");
            Console.WriteLine("Type={0}", mbrt.GetType());// CLR在类型上撒谎了

            // 证明得到的是对一个代理对象的引用
            Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbrt));

            // 看起来像是MarshalByRefType上调用一个方法,实则不然、
            // 我们是在代理类型上调用一个方法,代理使线程切换到拥有对象的
            // 那个AppDomain,并在真实的对象上调用这个方法
            mbrt.SomeMethod();

            // 卸载新的AppDomain
            AppDomain.Unload(ad2);

            // mbrt引用一个有效的代理对象;代理对象引用一个无效的AppDomain
            try
            {
                // 在代理类型上调用一个方法。AppDomain无效,造成抛出异常
                mbrt.SomeMethod();
                Console.WriteLine("Successful call.");
            }
            catch(AppDomainUnloadedException)
            {
                Console.WriteLine("Failed call.");
            }

            // *** DEMO 2: 使用Marshal-by-Value进行跨AppDomain通信 ***
            Console.WriteLine("{0}Demo #2", Environment.NewLine);

            // 新建一个AppDomain(从当前AppDomain继承安全性和配置)
            ad2 = AppDomain.CreateDomain("AppDomain#2", null, null);

            // 将我们的程序集加载到新AppDomain中,构造一个对象,把它
            // 封送回我们的AppDomain(实际得到对一个代理的引用)
            mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApp9.MarshalByRefType");

            // 对象的方法返回所返回对象的副本
            // 对象按值(而非按引用)封送
            MarshalByValType mbvt = mbrt.MethodWithReturn();

            // 证明得到的不是对一个代理对象的引用
            Console.WriteLine("Is proxy={0}", RemotingServices.IsTransparentProxy(mbvt));

            // 看起来是在MarshalByValType上调用一个方法,实际也是如此
            Console.WriteLine("Returned object created " + mbvt.ToString());

            // 卸载新的AppDomain
            AppDomain.Unload(ad2);
            // mbvt引用有效的对象;卸载AppDomain没有影响
            try
            {
                // 我们是在对象上调用一个方法;不会抛出异常
                Console.WriteLine("Returned object created " + mbvt.ToString());
                Console.WriteLine("Successful call.");
            }
            catch(AppDomainUnloadedException)
            {
                Console.WriteLine("Failed call.");
            }

            // *** DEMO 3: 使用不可封送的类型进行跨AppDomain通信 ***
            Console.WriteLine("{0}Demo #3", Environment.NewLine);

            // 新建一个AppDomain(从当前AppDomain继承安全性和配置)
            ad2 = AppDomain.CreateDomain("AppDomain#2", null, null);

            // 将我们的程序集加载到新AppDomain中,构造一个对象,把它
            // 封送回我们的AppDomain(实际得到对一个代理的引用)
            mbrt = (MarshalByRefType)ad2.CreateInstanceAndUnwrap(exeAssembly, "ConsoleApp9.MarshalByRefType");

            // 对象的方法返回一个不可封送的对象;抛出异常
            NonMarshalableType nmt = mbrt.MethodArgAndReturn(callingDomainName);
            // 这里永远执行不到...
        }
    }

    // 该类的实例可跨越AppDomain的边界“按引用封送”
    public sealed class MarshalByRefType : MarshalByRefObject
    {
        public MarshalByRefType()
        {
            Console.WriteLine("{0} ctor running in {1}",
                this.GetType().ToString(), Thread.GetDomain().FriendlyName);
        }

        public void SomeMethod()
        {
            Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName);
        }

        public MarshalByValType MethodWithReturn()
        {
            Console.WriteLine("Executing in "+ 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
    {
        // 注意:DateTime是可序列化的
        private DateTime m_creationTime = DateTime.Now;

        public MarshalByValType()
        {
            Console.WriteLine("{0} ctor running in {1}, Created on {2:D}",
                this.GetType().ToString(),
                Thread.GetDomain().FriendlyName,
                m_creationTime);
        }

        public override string ToString()
        {
            return m_creationTime.ToLongDateString();
        }
    }

    // 该类的实例不能跨AppDomain边界进行封送
    // [Serializable]
    public sealed class NonMarshalableType : Object
    {
        public NonMarshalableType()
        {
            Console.WriteLine("Executing in " + Thread.GetDomain().FriendlyName);
        }
    }
}

运行结果
11111.png

22222.png

现在来讨论下以上代码以及CLR所做的事情。
Marshalling方法首先获得一个AppDomain对象引用,当前调用线程正在该AppDomain中执行。在Windows中,线程总是在一个进程的上下文中创建,而且线程的整个生存期都在该进程的生存期内。但线程和AppDomain没有一对一关系。AppDomain是一项CLR功能;Windows对AppDomain一无所知。由于一个Windows进程可包含多个AppDomain,所以线程能执行一个AppDomain中的代码,再执行另一个AppDomain中的代码。从CLR的角度看,线程一次只能执行一个AppDomain中的代码。线程可调用System.Threading.Thread的静态方法GetDomain向CLR询问它正在哪个AppDomain中执行。线程还可查询System.AppDomain的静态只读属性CurrentDomain获得相同的信息。
AppDomain创建后可被赋予一个友好名称。它是用于标识AppDomain的一个String。友好名称主要是为了方便调试。由于CLR要在我们的任何代码执行前创建默认AppDomain,所以使用可执行文件的文件名作为默认的AppDomain友好名称。Marshalling方法使用System.AppDomain的只读FriendlyName属性来查询默认AppDomain的友好名称。
接着,Marshalling方法查询默认AppDomain中加载的程序集的强命名标识,这个程序集定义了入口方法Main(其中调用了Marshalling)。程序集定义了几个类型:Program,MarshalByRefType,MarshalByValType和NonMarshallableType。现在已准备好研究上面的三个演示(Demo),它们本质上很相似。

演示 1: 使用“按引用封送”进行跨AppDomain通信

演示1调用System.AppDomain的静态CreateDomain方法指示CLR在同一个Windows进程中创建一个新AppDomain。AppDomain类型提供了CreateDomain方法的多个重载版本。建议仔细研究一下它们,并在新建AppDomain时选择最合适的一个。本例使用的CreateDomain接受以下三个参数:
● 代表新AppDomain的友好名称的一个String。
● 一个System.Security.Policy.Evidence,是CLR用于计算AppDomain权限集的证据。本例为该参数传递null,造成新AppDomain从创建它的AppDomain继承权限集。通常,如果希望围绕AppDomain中的代码创建安全边界,可构造一个System.Security.PermissionSet对象,在其中添加希望的权限对象(实现了IPermission接口的类型的实例),将得到的PermissionSet对象引用传给接受一个PermissionSet的CreateDomain方法重载。
● 一个System.AppDomainSetup,代表CLR为新AppDomain使用的配置设置。同样,本例为该参数传递null,使新AppDomain从创建它的AppDomain继承配置设置。如果希望对新AppDomain进行特殊配置,可构造一个AppDomainSetup对象,将它的各种属性(例如配置文件的名称)设为你希望的值,然后将得到的AppDomainSetup对象引用传给CreateDomain方法。
CreateDomain方法内部会在进程中新建一个AppDomain,该AppDomain将被赋予指定的好友名称、安全性和配置设置。新AppDomaine有自己的Loader堆,这个堆目前是空的,因为还没有程序集加载到新AppDomain中。创建AppDomain时,CLR不在这个AppDomain中创建任何线程;AppDomain中也不会运行代码,除非显式地让一个线程调用AppDomain中的代码。
现在,要在新AppDomain中创建类型的实例,首先要将程序集加载到新AppDomain中,然后构造程序集中定义的类型的实例。这就是AppDomain的公共实例方法CreateInstanceAndUnwrap所做的事情。调用这个方法时我传递了两个String实参:第一个标识了想在新AppDomain(ad2变量引用的那个AppDomain)中加载的程序集;第二个标识了想构建其实例的那个类型的名称("MarshalByRefType")。在内部,CreateInstanceAndUnwrap方法导致调用线程从当前AppDomain切换新AppDomain。现在,线程(当前正在调用CreateInstanceAndUnwrap)将指定程序集加载到新AppDomain中,并扫描程序集的类型定义元数据表,查找指定类型("MarshalByRefType")。找到类型后,线程调用MarshalByRefType的无参构造器。现在,线程又切换回默认AppDomain,使CreateInstanceAndUnwrap能返回对新MarshalByRefType对象的引用。

注意  CreateInstanceAndUnwrap方法的一些重载版本允许在调用类型的构造器时传递实参。

所有这一切听起来都很美好,但还有一个问题:CLR不允许一个AppDomain中的变量(根)引用另一个AppDomain中创建的对象。如果CreateInstanceAndUnwrap直接返回对象引用,隔离性就会被打破,而隔离是AppDomain的全部目的!因此,CreateInstanceAndUnwrap在返回对象引用前要执行一些额外的逻辑。
我们的MarshalByRefType类型从一个很特别的基类System.MarshalByRefObject派生。当CreateInstanceAndUnwrap发现它封送的一个对象的类型派生自MarshalByRefObject时,CLR就会跨AppDomain边界按引用封送对象。下面讲述了按引用将对象从一个AppDomain(源AppDomain,这是真正创建对象的地方)封送到另一个AppDomain(目标AppDomain,这是调用CreateInstanceAndUnwrap的地方)的具体含义。
源AppDomain想向目标AppDomain发送或返回对象引用时,CLR会在目标AppDomain的Loader堆中定义一个代理类型。代理类型是用原始类型的元数据定义的。所以,它看起来和原始类型完全一样;有完全一样的实例成员(属性、事件和方法)。但是,实例字段不会成为(代理)类型的一部分,代理类型确实定义了几个(自己的)实例字段,但这些字段和原始类型的不一致。相反,这些字段只是指出哪个AppDomain"拥有"真实的对象,以及如何在拥有(对象的)AppDomain中找到真实的对象。(在内部,代理对象用一个GCHandle实例引用真实的对象。)
在目标AppDomain中定义好这个代理类型之后,CreateInstanceAndUnwrap方法就会创建代理类型的实例,初始化它的字段来标识源AppDomain和真实对象,然后将对这个代理对象的引用返回给目标AppDomain。在上面的示例中,mbrt变量被设为引用这个代理。注意,从CreateInstanceAndUnwrap方法返回的对象实际不是MarshalByRefType类型的实例。CLR一般不允许将一个类型的对象转换成不兼容的类型。但在当前这种情况下,CLR允许转型,因为新类型具有和原始类型一样的实例成员。事实上,用代理对象调用GetType,它会向你撒谎,说自己是一个MarshalByRefType对象。
但可以证明从CreateInstanceAndUnwrap返回的对象实际是对代理的引用。为此,应用程序调用了System.Runtime.Remoting.RemotingService的公共静态IsTransparentProxy方法并向其传递CreateInstanceAndUnwrap方法返回的引用。从输出结果可知,IsTransparentProxy方法返回true,证明返回的是代理。
接着,应用程序使用代理调用SomeMethod方法。由于mbrt变量引用代理对象,所以会调用由代理实现的SomeMethod。代理的实现利用代理对象中的信息字段,将调用线程从默认AppDomain切换至新AppDomain。现在,该线程的任何行动都在新AppDomain的安全策略和配置设置下运行。线程接着使用代理对象的GCHandle字段查找新AppDomain中的真实对象,并用真实对象调用真实的SomeMethod方法。
有两个办法可证明调用线程已从默认AppDomain切换至新AppDomain。首先,我在SomeMethod方法中调用了Thread.GetDomain().FriendlyName。这将返回"AD #2",这是由于线程当前在新AppDomain中运行,而这个新AppDomain是通过调用AppDomain.CreateDomain方法,并传递"AD #2"作为友好名称参数来创建的。其次,在调试器中逐语句调试代码,并打开了“调用堆栈”窗口,那么“[外部代码]”行会标注一个线程在什么位置跨越AppDomain边界。
11111.png
真实的SomeMethod方法返回后,会返回至代理的SomeMethod方法,后都将线程切换回默认AppDomain。线程继续执行默认AppDomain中的代码。

注意  一个AppDomain中的线程调用另一个AppDomain中的方法时,线程会在这两个AppDomain之间切换。这意味着跨AppDomain边界的方法调用是同步执行的(一个AppDomain的方法执行完毕,才能执行另一个AppDomain的方法。不能多个AppDomain的代码并发执行)。任何时刻一个线程只能在一个AppDomain中,而且要用那个AppDomain的安全和配置设置来执行代码。如果希望多个AppDomain中的代码并发执行,应创建额外的线程,让这些线程在你希望的AppDomain中执行你希望的代码。

应用程序接着做的事情是调用AppDomain类的公共静态Unload方法,这会强制CLR卸载指定的AppDomain(包括加载到其中的所有程序集),并强制执行一次垃圾回收,以释放由卸载的AppDomain中的代码创建的所有对象。这时,默认AppDomain的mbrt变量仍然引用一个有效的代理对象。但代理对象已不再引用一个有效的AppDomain(因为它已经卸载了)。
当默认AppDomain试图使用代理对象调用SomeMethod方法时。调用的是该方法在代理中的实现。代理的实现发现包含真实对象的AppDomain已卸载。所以,代理的SomeMethod方法抛出一个AppDomainUnloadedException异常,告诉调用者操作无法完成。
显然,Microsoft的CLR团队不得不做大量的工作来确保AppDomain的正确隔离,但这是他们必须做的。跨AppDomain访问对象的功能正在被大量地使用,开发人员对这个功能的依赖性正在日益增加。不过,使用“按引用封送”的语义进行跨AppDomain边界的对象访问,会产生一些性能上的开销。所以,一般应尽量少用这个功能。
从MarshalByRefObject派生的类型可定义实例字段。但这些实例字段不会成为代理类型的一部分,也不会包含在代理对象中。写代码对派生自MarshalByRefObject的类型的实例字段进行读写时,JIT编译器会自动生成代码,分别调用System.Object的FieldGetter方法(用于读)或FieldSetter方法(用于写)来使用代理对象(以找到真正的AppDomain/对象)。这些方法是私有的,而且没有在文档中记录。简单地说,这些方法利用反射机制获取或设置字段值。因此,虽然能访问派生自MarshalByRefObject的一个类型中的字段,但性能很差,因为CLR最终要调用方法来执行字段访问。事实上,即使要访问的字段在你自己的AppDomain中,性能也好不到哪里去。(如果CLR要求所有字段都必须私有(为了获得好的数据封装,我强烈建议这样做),那么FieldGetter和FieldSetter方法根本没有存在地必要,从方法中总是能够直接访问字段,避免(因为还要经由中间的getter或setter方法)造成性能损失)。

访问实例字段时的性能测试

using System;
using System.Diagnostics;

namespace ConsoleApp10
{
    class Program
    {
        static void Main(string[] args)
        {
            FieldAccessTiming();
            Console.Read();
        }

        private static void FieldAccessTiming()
        {
            const Int32 count = 1000000000;
            NonMBRO nonMbro = new NonMBRO();
            MBRO mbro = new MBRO();

            Stopwatch sw = Stopwatch.StartNew();
            for (Int32 c = 0; c < count; c++) nonMbro.x++;
            Console.WriteLine("{0}", sw.Elapsed);

            sw = Stopwatch.StartNew();
            for (Int32 c = 0; c < count; c++) mbro.x++;
            Console.WriteLine("{0}", sw.Elapsed);
        }

        private sealed class NonMBRO : Object { public Int32 x; }
        private sealed class MBRO : Object { public Int32 x; }
    }
}

运行测试
1111.png

从好不好用(usability)的角度说,派生自MarshalByRefObject的类型应该避免定义任何静态成员。这是因为静态成员总是在调用AppDomain的上下文中访问。要切换到哪个AppDomain的信息包含在代理对象中,但调用静态成员时没有代理对象,所以不会发生AppDomain的切换。让类型的静态成员在一个AppDomain中执行,实例成员却在另一个AppDomain中执行,这样的编程模型未免太“丑”了!
由于第二个AppDomain中没有根,所以代理引用的原始对象可以被垃圾回收。这当然不理想。但另一方面,假如将原始对象不确定地(indefinitely)留在内存中,代理可能不再引用它,而原始对象依然存活;这同样不理想。CLR解决这个问题的办法是使用一个“租约管理器”(lease manager)。一个对象的代理创建好之后,CLR保持对象存活5分钟。5分钟内没有通过代理发出调用,对象就会失效,下次垃圾回收会释放它的内存。每发出一次对象的调用,“租约管理器”都会续订对象的租期,保证它在接下来的2分钟内在内存中保持存活。在对象过期后试图通过代理调用它,CLR会抛出System.Runtime.Remoting.RemotingException异常。
默认的5分钟和2分钟租期设定是可以修改的,重写MarshalByRefObject的虚方法InitializeLifetimeService即可。

演示 2:使用“按值封送”进行跨AppDomain通信

演示2和演示1很相似。和演示1一样,演示2也创建了新AppDomain。然后调用CreateInstanceAndUnwrap方法将同一个程序集加载到新建AppDomain中,并在这个新AppDomain中创建MarshalByRefType类型的实例。CLR为这个对象创建代理,mbrt变量(在默认AppDomain中)被初始化成引用这个代理。接着用代理调用MethodWithReturn方法。这个方法是无参的,将在新AppDomain中执行以创建MarshalByValType类型的实例,并将一个对象引用返回给默认AppDomain。
MarshalByValType不从System.MarshalByRefObject派生,所以CLR不能定义一个代理类型并创建代理类型的实例;对象不能按引用跨AppDomain边界进行封送。
但由于MarshalByValType标记了自定义特性[Serializable],所以MethodWithReturn方法能按值封送对象。下面具体描述了将一个对象按值从一个AppDomain(源AppDomain)封送到另一个AppDomain(目标AppDomain)的含义。
源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR将对象的实例字段序列化成一个字节数组。字节数组从源AppDomain复制到目标AppDomain。然后,CLR在目标AppDomain中反序列化字节数组,这会强制CLR将定义了“被反序列化的类型”的程序集加载到目标AppDomain中(如果尚未加载的话)。接着,CLR创建类型的实例,并利用字节数组中的值初始化对象的字段,使之与源对象中的值相同。换言之,CLR在目标AppDomain中精确复制了源对象。然后,MethodWithReturn方法返回对这个副本的引用;这样一来,对象就跨AppDomain的边界按值封送了。

重要提示  加载程序集时,CLR使用目标AppDomain的策略和配置设置(而AppDomain可能设置了不同的AppBase目录或者不同的版本绑定重定向)。策略上的差异可能妨碍CLR定位程序集。程序集无法加载时会抛出异常,目标AppDomain接收不到对象引用。

至此,源AppDomain中的对象和目标AppDomain中的对象就有了独立的生存期,它们的状态也可以独立地更改。如果源AppDomain中没有根保持源对象存活,源对象的内存就会在下次垃圾回收时被回收。
为了证明MethodWithReturn方法返回的不是对代理对象的引用,程序调用了System.Runtime.Remoting.RemotingService的公共静态IsTransparentProxy方法,将MethodWithReturn方法返回的引用作为参数传给它。IsTransparentProxy方法返回false,表明对象是一个真实的对象,不是代理。
现在,程序使用真实的对象调用ToString方法。由于mbvt变量引用真实的对象,所以会调用这个方法的真实实现,线程不会在AppDomain之间切换。为了证明这一点,可查看调试器的“调用堆栈”窗口,它没有显示一个“[外部代码]”行。
为了进一步证实没有涉及代理,程序卸载了AppDomain,然后尝试再次调用ToString。和演示1不一样,这次调用会成功,因为卸载新AppDomain对默认AppDomain“拥有”的对象(其中包括按值封送的对象)没有影响。

演示 3:使用不可封送的类型跨AppDomain通信

演示3的开始部分与演示1和2相似,都是新建AppDomain,调用CreateInstanceAndUnwrap方法将相同的程序集加载到新AppDomain中,在新AppDomain中创建一个MarshalByRefType对象,并让mbrt引用该对象。
然后,用代理调用接受一个实参的MethodArgAndReturn方法。同样地,CLR必须保持AppDomain的隔离,所以不能直接将对实参的引用传给新AppDomain。如果对象的类型派生自MarshalByRefObject,CLR会为它创建代理并按引用封送。如果对象的类型用[Serializable]进行了标记,CLR会将对象(及其子)序列化成一个字节数组,将字节数组封送到新AppDomain中,再将字节数组反序列化成对象图,将对象图的根传给MethodArgAndReturn方法。
在这个特定的例子中,跨越AppDomain边界传递一个System.String对象。System.String类型不是从MarshalByRefObject派生的,所以CLR不能创建代理。幸好,System.String被标记为[Serializable],所以CLR能按值封送它,使代码能正常工作。注意,对于String对象,CLR会采取特殊的优化措施。跨越AppDomain边界封送一个String对象时,CLR只是跨越边界传递对String对象的引用;不会真的生成String对象的副本。之所以能提供这个优化,是因为String对象是不可变的;所以,一个AppDomain中的代码不可能破坏String对象的字段。(顺便说一句,这正是为什么System.String类是密封类的原因。类不密封,就能定义从String派生的类,并添加自己的字段。如果这样做了,CLR就没有办法确保你的“字符串”类是不可变的)
在MethodArgAndReturn内部,显示传给它的字符串,证明字符串跨越了AppDomain边界。然后,我创建NonMarshalableType类型的实例,并将对这个对象的引用返回至默认AppDomain。由于NonMarshalableType不是从System.MarshalByRefObject派生的,也没有用[Serializable]定制特性进行标记,所以不允许MethodArgAndReturn按引用或按值封送对象——对象完全不能跨越AppDomain边界进行封送!为了报告这个问题,MethodArgAndReturn在默认AppDomain中抛出一个SerializationException异常。由于程序没有捕捉这个异常,所以程序终止。

三、卸载AppDomain

AppDomain很强大的一个地方就是可以卸载它。卸载AppDomain会导致CLR卸载AppDomain中的所有程序集,还会释放AppDomain的Loader堆。卸载AppDomain的办法是调用AppDomain的静态Unload方法。这导致CLR执行一系列操作来得体地卸载指定的AppDomain。
1. CLR挂起进程中执行过托管代码的所有线程。
2. CLR检查所有线程栈,查看哪些线程正在执行要卸载的AppDomain中的代码,或者哪些线程会在某个时候返回至要卸载的AppDomain。任何栈上有要卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException(同时恢复线程的执行)。这将导致线程展开(unwind),并执行遇到的所有finally块以清理资源。如果没有代码捕捉ThreadAbortException,它最终会成为未处理的异常,CLR会“吞噬”这个异常;线程会终止,但进程可继续运行。这是很特别的一点,因为对于其他所有未经处理的异常,CLR都会终止进程。

重要提示  如果线程当前正在finally块、catch块、类构造器、临界执行区域或非托管代码执行,那么CLR不会立即终止该线程。否则,资源清理代码、错误恢复代码、类型初始化代码、关键(critical)代码或者其他任何CLR不了解的代码都将无法完成,导致应用程序的行为无法预测,甚至可能造成安全漏洞。线程终止时会等待这些代码块执行完毕。然后,当代码块结束时,CLR再强制抛出一个ThreadAbortException异常。

译注:critical execution region,翻译为“关键执行区域”或“临界执行区域”或“临界区”。临界区是指线程终止或未处理异常的影响可能不限于当前任务的区域。相反,非临界区中的终止或失败只对出错的任务有影响。

3. 当第2步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“由已卸载的AppDomain创建的对象”的每个代理对象都设置一个标志(flag)。这些代理对象现在知道它们引用的真实对象已经不在了。现在,任何代码在无效的代理对象上调用方法都会抛出一个AppDomainUnloadedException异常。
4. CLR强制垃圾回收,回收由已卸载的AppDomain创建的任何对象的内存。这些对象的Finalize方法被调用,使对象有机会正确清理它们占用的资源。
5. CLR恢复剩余所有线程的执行。调用AppDomain.Unload方法的线程将继续运行;对AppDomain.Unload的调用是同步进行的。

顺便说一句,当一个线程调用AppDomain.Unload方法时,针对要卸载的AppDomain中的线程,CLR会给它们10秒钟的时间离开。10秒钟后,如果调用AppDomain.Unload方法的线程还没有返回,CLR将抛出一个CannotUnloadAppDomainException异常,AppDomain将来可能会、也可能不会卸载。

注意  如果调用AppDomain.Unload方法的线程不巧在要卸载的AppDomain中,CLR会创建另一个线程来尝试卸载AppDomain。第一个线程被强制抛出ThreadAbortException并展开(unwind)。新线程将等待AppDomain卸载,然后新线程会终止。如果AppDomain卸载失败,新线程将抛出CannotUnloadAppDomainException异常。但是,由于我们没有写由新线程执行的代码,所以无法捕捉这个异常。

标签: C#

Powered by emlog  蜀ICP备18021003号   sitemap

川公网安备 51019002001593号