• 【CLR】解析AppDomain


    目录结构:

    contents structure [+]

    1.什么是AppDomain

    在开始介绍AppDomain之前,需要先介绍一下应用程序和AppDomain的关联。首先,任何Windows应用程序都能寄宿在CLR中,寄宿CLR需要创建CLR COM服务器的实例(通过MSCorEE.dll文件)。CLR COM服务器实例在初始化时,会创建一个默认的AppDomain。

    AppDomain就是一组程序集的容器。AppDomain是为隔离而设计的,除了默认的AppDomain。正在使用非托管COM接口方法或托管类型的宿主应用程序还可要求CLR创建其他的AppDomain。

    AppDomain具有如下的一些特点:
    1.一个AppDomain中的代码不能直接访问另一个AppDomain代码创建的对象。
    2.AppDomain可以卸载。

    下面的图片显示了一个Windows进程,运行着一个CLR COM服务器。该CLR运行着两个AppDomain。


    2.跨越AppDomain边界访问对象

    一个AppDomain中的代码可以和另一个AppDomain中的类型和对象通信,但只能通过良好定义的机制进行。通信的机制主要为按“引用封送(Marshal-by-Reference)”,和“按值封送(Marshal-by-Value)”,并不是所有类型都可以跨越AppDomain进行通信。接下里将接详细介绍。

    2.1 按引用封送(Marshal-by-Reference)

    #include "stdafx.h"
    #include <exception>
    
    using namespace System;
    using namespace System::Threading;
    using namespace System::Reflection;
    using namespace System::Runtime::Remoting;
    using namespace std;
    
    //该类实例可以跨AppDomain边界“按引用封送”
    public ref class MarshalByRefType : System::MarshalByRefObject{
    public:
        MarshalByRefType(){//定义构造函数
            Console::WriteLine("{0} ctor running in {1}",
                GetType()->ToString(),
                Thread::GetDomain()->FriendlyName);
        }
    public:
        void SomeMethod(){//定义一个方法
            Console::WriteLine("Executing in "+Thread::GetDomain()->FriendlyName);
        }
    };
    
    int main(array<System::String ^> ^args)
    {
        //获取AppDomain的引用
        AppDomain^ adCallingThreadDomain=Thread::GetDomain();
    
        //获取这个AppDomain的友好字符串名称
        String^ callingDomainThread= adCallingThreadDomain->FriendlyName;
        Console::WriteLine("Default AppDomain's friendly name={0}",callingDomainThread);//显示AppDomain的名称
        
        //获取并显示AppDomain中包含“Main”方法的程序集
        String^ exeAssembly= Assembly::GetEntryAssembly()->FullName;
        Console::WriteLine("Main assembly={0}",exeAssembly);//包含了程序集的名称、语言文化、公钥、版本信息
    
        //按引用封送Marshal-by-Reference,进行跨AppDomain通信
        
        //新建一个AppDomain(从当前AppDomain继承安全性和配置)
        AppDomain^ ad2=AppDomain::CreateDomain("ad2");
    
        //将我们的程序集加载到新的AppDomain中,构建一个对象,并且把它封送回我们的AppDomain(实际得到的是一个代理的引用)
        MarshalByRefType^ mbrt=(MarshalByRefType^)ad2->CreateInstanceAndUnwrap(MarshalByRefType::typeid->Assembly->FullName,"MarshalByRefType");
    
        Console::WriteLine("Type = {0}",mbrt->GetType());//CLR 在类型上撒谎,实际上是MarshalByRefType的代理类型,但却返回了MarshalByRefType类型。
    
        Console::WriteLine("Is Proxy={0}",RemotingServices::IsTransparentProxy(mbrt));//证明得到是对一个代理对象的引用
    
        //看起来像是在MarshalByRefType上调用一个方法,实则不然,
        //我们在代理类型上调用一个方法,代理使线程切换到拥有对象的
        //那个AppDomain上,并在真实的对象上调用这个方法
        mbrt->SomeMethod();
    
        //卸载新的AppDomain
        AppDomain::Unload(ad2);
    
        //mbrt 引用一个有效的代理对象;代理对象引用一个无效的AppDomain
        try{
            mbrt->SomeMethod();//在代理对象上调用这个方法。AppDomain无效,造成抛出异常。
        }catch(AppDomainUnloadedException^ e){
            Console::WriteLine(e->Message);
        }
        Console::ReadLine();
        return 0;
    }

    运行结果:

    Default AppDomain's friendly name=ConsoleApplication13.exe
    Main assembly=ConsoleApplication13, Version=1.0.6761.1527, Culture=neutral, PublicKeyToken=null
    MarshalByRefType ctor running in ad2
    Type = MarshalByRefType
    Is Proxy=True
    Executing in ad2
    尝试访问已卸载的 AppDomain。

    在上面的程序中,我们实现了在一个AppDomain中读取另一个AppDomain中的对象,该对象继承自MarshalByRefObject。


    观察上面的运行结果,可以看出有关MarshalByRefType的操作都是在创建它的AppDomain中完成的,并且在当前的AppDomain中得到的MarshalByRefType是一个代理对象,而非真实的对象。

    2.2 按值封送(Marshal-by-Value)

    我们已经知道了AppDomain之间如何按引用封送对象,按值封送和按引用封送的调用代码都是相同的,它们的区别是由被封送的类型决定的。

    按引用封送:被封送的类型派生自System::MarshalByRefObject

    按值封送:被封送的类型不派生自System::MarshalByRefObject,并且该类和该类的所有字段都必须是可序列化的。

    下来继续介绍按值封送。

    using namespace System;
    using namespace System::Threading;
    using namespace System::Runtime::Remoting;
    
    //声明为序列化,否则不能传递
    [Serializable]
    public ref class MarshalByRefValue : Object{
    public:
        DateTime^ m_createTime;
        MarshalByRefValue(){
            m_createTime=DateTime::Now;
    
            Console::WriteLine("{0} ctor running in {1},Created on {2:D}",
                GetType()->ToString(),
                Thread::GetDomain()->FriendlyName,
                m_createTime);
        }
        virtual String^ ToString() override{
            Console::WriteLine("running in "+AppDomain::CurrentDomain->FriendlyName);
            return m_createTime->ToLongDateString();
        }
    };
    
    int main(array<System::String ^> ^args)
    {
        //使用Marshel-by-value进行通信
    
        //得到一个AppDomain
        AppDomain^ ad2=AppDomain::CreateDomain("ad2");
    
        //按值传递
        MarshalByRefValue^ mbrv=(MarshalByRefValue^) ad2->CreateInstanceAndUnwrap(MarshalByRefValue::typeid->Assembly->FullName,"MarshalByRefValue");
    
        Console::WriteLine("Is Proxy={0}",RemotingServices::IsTransparentProxy(mbrv));//false,说明得到的是实际引用,而非代理引用
    
        //调用方法
        Console::WriteLine(mbrv->ToString());
    
        AppDomain::Unload(ad2);
    
        try{
            Console::WriteLine(mbrv->ToString());
            Console::WriteLine("execute success");
        }catch(AppDomainUnloadedException^ e){
            Console::WriteLine(e->Message);
        }
    
        Console::ReadLine();
        return 0;
    }

    运行结果:

    MarshalByRefValue ctor running in ad2,Created on 2018年7月6日
    Is Proxy=False
    running in ConsoleApplication14.exe
    2018年7月6日
    running in ConsoleApplication14.exe
    2018年7月6日
    execute success

    通过上面的程序,我们不难观察出,按值传递的类型必须要序列化,否则不能传递(抛出异常)。我能定义了MarshalByRefValue类,该类和其字段(DateTime^ m_createTime)都是可序列化的。
    在一个AppDomain中,把需要传递的类型和字段进行序列化,传到目前的AppDomain中,然后再进行反序列化,重新构建对象。
    观察上面的程序的结果,可以看出MarshalByRefValue的所有操作都是在main方法的AppDomain中完成的,所以按值传送是把原来的AppDomain的对象序列化到目前的调用的AppDomain中。

    3.卸载AppDomain

    在上面的代码中,已经尝尝试过进行AppDomain的卸载操作。并不是所有的AppDomain都由程序员进行卸载(CLR COM服务器实例启动时默认创建的AppDomain直到应用程序结束时才由系统自动卸载)。卸载AppDomain会导致CLR卸载AppDomain中的所有的程序集,还会释放AppDomain中的loader堆。

    卸载AppDomain的方法非常简单,可以直接调用AppDomain中的Unload方法。

    4.监视AppDomain

    宿主程序可监视AppDomain消耗的资源。有的宿主可根据这种信息判断一个AppDomain的内存或CPU消耗是否超过了应有的水准,并强制卸载它。
    开始监视AppDomain的时候,需要将AppDomain的静态MonitorEnabled属性设置为true,从而显示打开监视。监视一旦打开就不能关闭,将MonitorEnabled设为false将会抛出ArgumentException异常。

    AppDomain提供了以下四个只读属性来监视:
    MonitoringSurvivedProcessMemorySize,这个Int64的静态属性返回当前CLR实例控制的所有AppDomain使用的字节数。这个数字只保证在上次垃圾回收时是准确的。
    MonitoringTotalAllocatedMemorySize,这个Int64的实例属性返回特定AppDomain已分配的字节数。这个数字只保证在上一次回收时是准确的。
    MonitoringSurvivedMemorySize,这个Int64实例属性返回特定AppDomain当前正在使用的字节数。这个数字只保证在上一次回收时是准确的。
    MonitoringTotalProcessorTime,这个TimeSpan实例属性返回特定AppDomain的CPU占有率。

    下面这个类检查两个时间点之间一个AppDomain发生的变化:

    ref class AppDomainMonitorDelta{
     private:
         AppDomain^ m_appDomain;
         TimeSpan  m_thisADCpu;
         Int64 m_thisADMemoryInUse;
         Int64 m_thisADMemoryAllocated;
     public:
         static AppDomainMonitorDelta(){
             AppDomain::MonitoringIsEnabled=true;
         }
         AppDomainMonitorDelta(AppDomain^ appDomain){
             this->m_appDomain=appDomain;
             this->m_thisADCpu=m_appDomain->MonitoringTotalProcessorTime;
             this->m_thisADMemoryInUse=m_appDomain->MonitoringSurvivedMemorySize;
             this->m_thisADMemoryAllocated=m_appDomain->MonitoringTotalAllocatedMemorySize;
         }
    
         ~AppDomainMonitorDelta(){//定义析构函数
             GC::Collect();
    
             Console::WriteLine("FriendlyName={0},CPU={1}ms",
                 m_appDomain->FriendlyName,
                 m_appDomain->MonitoringTotalProcessorTime.Subtract(m_thisADCpu));
    
             Console::WriteLine("Allocated {0:N0} bytes of which {1:N0} survived GCs",
                 (m_appDomain->MonitoringTotalAllocatedMemorySize-m_thisADMemoryAllocated),
                 (m_appDomain->MonitoringSurvivedMemorySize-m_thisADMemoryInUse));
    
         }
    };

    然后就可以按照如下的姿势来调用了:

    int main(array<System::String ^> ^args)
    {
        AppDomainMonitorDelta^ appDomainMonitor=gcnew AppDomainMonitorDelta(AppDomain::CurrentDomain);//监控当前AppDomain
    
        List<Object^>^ list=gcnew List<Object^>();
        for(Int32 x=0;x<10000;x++){
            list->Add(gcnew Object());
        }
    
        Int64 stop=Environment::TickCount+5000;//Environment::TickCount 获取系统启动后经过的毫秒数
        while(Environment::TickCount<stop);//持续工作5秒钟
    
        delete appDomainMonitor;//使用delete释放指针所指向的内存空间
        Console::ReadLine();
        return 0;
    }

    5.AppDomainFirstChance异常通知

    每一个AppDomain都可以关联一组回调方法;CCLR开始查找AppDomain中的catch块时,这些回调方法得以调用。可用这些方法执行日志记录。另外,宿主可用这个机制监视AppDomain中抛出的异常。回调方法不能处理异常,也不能以任何形式吞噬异常;他们只是接受关于异常发生的通知。登记回调方法,只需要为AppDomain的实例事件FirstChanceException添加一个委托就可以了。

    异常首次抛出时,CLR调用向抛出异常的AppDomain登记所有FirstChanceException回调方法。然后,CLR查找栈上在同一个AppDomain中的任何catch块。有一个catch块能处理异常,则异常处理完成,将继续正常执行,如果AppDomain中没有一个catch块能处理异常,则CLR沿着栈向上来调用AppDomain,再次抛出同一个异常对象(序列化和反序列化后)。这时感觉像抛出了一个全新的异常,CLR调用向当前AppDomain登记的所有FirstChanceException回调方法。这个过程会一直持续,直到抵达线程栈顶部。如果异常还未被处理,CLR只好终止整个进程。

    using namespace System;
    using namespace System::Reflection;
    using namespace System::Runtime::Remoting;
    using namespace System::Threading;
    using namespace System::Runtime::ExceptionServices;
    using namespace std;
    
    [Serializable]
    public ref class TestClass :Object{
    public:
        void someMethod(){
            try{
                int i=5;
                int j=0;
                //int k=i/j;
                throw "abc";
            }catch(InvalidCastException^ e){
                Console::WriteLine("{0} in remote AppDomain:{1}",AppDomain::CurrentDomain->FriendlyName,e->Message);
            }
        }
    };
    static void Test(Object^ sender,FirstChanceExceptionEventArgs^ args){
        Console::WriteLine("{0} in FirstChanceException",AppDomain::CurrentDomain->FriendlyName);
    };
    int main(array<System::String ^> ^args)
    {
        //绑定一个FirstChanceException事件
        AppDomain::CurrentDomain->FirstChanceException += gcnew EventHandler<FirstChanceExceptionEventArgs^>(Test);
    
        //创建一个AppDomain,保留和当前AppDomain一样的配置
        AppDomain^ ad2=AppDomain::CreateDomain("ad2");
    
        //得到实例
        TestClass^ mbro=(TestClass^)ad2->CreateInstanceAndUnwrap(TestClass::typeid->Assembly->FullName,TestClass::typeid->FullName);
    
        Console::WriteLine("Is Proxy={0}",RemotingServices::IsTransparentProxy(mbro));//false
    
        try{
            //调用方法
            mbro->someMethod();
        }catch(Exception^ e){
            Console::WriteLine("{0} in main:{1}",AppDomain::CurrentDomain->FriendlyName,e->Message);
        }
    
        Console::ReadLine();
        return 0;
    }

    输出结果为:

    Is Proxy=False
    ConsoleApplication15.exe in FirstChanceException
    ConsoleApplication15.exe in main:外部组件发生异常。

    可以看出,FirstChanceException上登记的委托方法先执行,然后再执行catch块中的代码。


  • 相关阅读:
    向iphone模拟器中导入图片
    键盘样式风格设置及隐藏
    (转)iPhone图片处理:摄像头/相册获取图片,压缩图片,上传服务器,下载,拉伸,方法总结
    python通过get方式,post方式发送http请求和接收http响应-urllib urllib2
    Python中 如何使用telnet 检测端口是否通
    Linux运维人员共用root帐户权限审计
    python ssh登录
    欢迎来到 Flask 的世界
    大杂烩
    随笔
  • 原文地址:https://www.cnblogs.com/HDK2016/p/9276156.html
Copyright © 2020-2023  润新知