DoubleJ

DoubleJ 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

DoubleJ 发布了文章 · 3月6日

设计模式

什么是设计模式?

设计模式是一套被反复使用的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重复使用代码,让代码更容易被他人理解并且提高代码的可靠性。

可以说设计模式是在特定环境下为解决某一通用软件设计问题提供的一套定制的解决方案,该方案描述了对象和类之间的相互作用。

基本要素
名称

模式名称是用来反映描述模式的问题,解决方案和效果,方便开发人员之间的交流与更好的理解设计模式,大多数设计模式都是根据功能来命名的,如xxxFactory。

问题

描述了应该在何时使用模式,包含了原始设计中存在的问题以及问题存在的原因。

解决方案

描述了设计模式的组成部分,以及这些组成部分之间的相互关系,各自的职责和协作方式。

效果

描述了设计模式的应用情况以及在使用模式时应该权衡的问题,包含设计模式的优缺点。没有任何一个解决方案是完美的,因此在选择设计模式的时候需要进行合理的选择。

设计模式的分类

可以根据目的(用来做什么的)分类,将模式分为:创建型、结构型、行为型。也可以根据范围(处理类之间的关系还是处理对象之间的关系)分类,将模式分为:类模式、对象模式,如下所示:
image.png

设计模式说明
抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类

建造者模式

将一个复杂对象的构建与它的表现分离,使得同样的构建过程可以创建不同的表示

工厂方法模式

定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式将一个类的实例化延迟到其子类

原型模式

使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象

单例模式

确保一个类只有一个实例,并提供一个全局访问点来访问该实例

适配器模式

将一个类的接口转换成客户希望的另一个接口,适配器模式让那些接口不兼容的类可以一起工作

桥接模式

将抽象部分与实现部分解耦,使得两者都能够独立变化

组合模式

组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式可以让客户端统一对待单个对象和组合对象

装饰模式

动态的给一个对象增加一些额外的职责,就扩展功能而言,装饰模式提供了一种比使用子类更加灵活的替代方案

外观模式

为子系统中的一组接口提供统一的入口,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用

享元模式

运用共享技术有效地支持大量细粒度对象的复用

代理模式

给某一个对象提供一个代理或占位符,并由代理对象来控制原对象的访问

责任链模式

避免一个请求的发送者和接收者偶合在一起,让多个对象都有机会处理请求。将接收请求的对象连接成一条链,并沿着这条链传递请求,直到有一个对象能够处理它为止

命令模式

将一个请求封装成一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销操纵

解释器模式

给定一种语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子

迭代器模式

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示

中介者模式

定义一个对象来封装一系列对象的交互,中介者模式使各个对象之间不需要显示地相互引用,从而使其耦合松散,让你可以独立地改变它们之间的交互

备忘录模式

在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态

观察者模式

定义对象之间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新

状态模式

允许一个对象在其内部状态改变时改变它的行为

策略模式

定义一系列算法,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法可以独立于使用它的客户变化

模板方法模式

定义一个操作中算法的框架,而将一些步骤延迟到子类中,模板方法模式使得子类可以不改变一个算法的结构就可以重定义该算法的某些特定步骤

访问者模式

表示一个作用于某对象结构中的各个元素的操作,访问者模式让你可以在不改变各个元素的类的前提下定义作用于这下元素的新操作

设计模式的优点
  1. 融合了众多专家的经验,并以一种标准的形式给广大开发人员使用,提供通用的设计词汇和开发语言,方便开发人员交流。让设计方案更加通俗易懂,使不同编程语言的开发和设计人员可以通过设计模式来交流系统设计方案,设计模式可以降低开发人员理解系统的复杂度。
  2. 让开发人员可以更简单地复用成功的设计和体系结构,使得重用成功的设计更加容易,并避免导致不可重用的设计方案。
  3. 让设计方案更加灵活,易于修改。在很多设计模式中广泛使用了开闭原则、依赖倒置原则等面向对象设计原则,让系统有较好的可维护性,真正实现了可维护性的复用。在软件开发中合理使用设计模式,可以使系统中的一些组成部分在其它系统得到重用,而在此基础上进行二次开发自然方便得多。
  4. 设计模式是通过多次实践证明的有效解决方案,且通常是针对某一类问题的最佳设计方案,因此设计模式的使用提高软件系统的开发效率和软件质量,在一定程度上节约设计成本。
  5. 有助于开发人员更深入地理解面向对象思想,还可以提高开发人员的设计水平和代码质量。
查看原文

赞 2 收藏 2 评论 0

DoubleJ 发布了文章 · 2月25日

线程安全Ⅱ

混合模式

由于用户模式和内核模式各有优劣,为了利用两者各自的有点,因此可以同时使用两种模式来进行构造,在没有线程竞争的时候可以具有用户模式的性能优势,而在多个线程同时竞争一个构造的时候又能提供不产生自旋的优点,使应用程序的性能得到提升。

示例代码
class HybridLock : IDisposable
{
    private int m_Waiters = 0;

    private AutoResetEvent m_WaiterLock = new AutoResetEvent(false);

    public void Enter()
    {
        //线程想要获得锁
        if (Interlocked.Increment(ref this.m_Waiters) == 1)
            return; //锁可以使用直,接返回

        //另一个线程正在等待,阻塞该线程
        this.m_WaiterLock.WaitOne(); //产生较大的性能影响
        //WaitOne返回后,这个线程便拥有了锁
    }

    public void Leave()
    {
        //这个线程准备释放锁
        if (Interlocked.Decrement(ref this.m_Waiters) == 0)
            return; //没有其它线程阻塞,直接返回

        //有其它线程正在阻塞,唤醒其中一个
        this.m_WaiterLock.Set(); //产生较大的性能影响
    }

    public void Dispose()
    {
        this.m_WaiterLock.Dispose();
    }
}
分析代码

HybridLock对象的Enter方法调用了Interlocked.Increment,让m_Waiters字段递增1,这个线程发现没有线程拥有这个锁,因此该线程获得锁并直接返回(获得锁的速度非常快)。如果另一个线程介入并调用Enter,此时m_Waiters字段再次递增值为2,发现锁被另一个线程拥有,所以这个线程会调用AutoResetEvent对象的WaitOne来阻塞本身(这里由于是内核模式所以会产生性能影响),防止自旋。

再看看Leave方法,一个线程调用Leave时,会调用Interlocked.Decrement使m_Waiters字段递减1,如果m_Waiters字段是0,说明没有其它线程在Enter的调用中发生阻塞,这时线程可以直接返回。但是如果线程发现m_Waiters字段的值递减后不为0,说明存在线程竞争,至少有一个线程在内核中阻塞,这个线程必须只能唤醒一个阻塞的线程,通过调用AutoResetEvent对象的Set方法实现,

自旋、线程所有权和递归

由于转换成内核代码会造成性能损失,而线程占有一个锁的时间通常比较短,所以可以先让线程处于用户模式且自旋一段时间,若还未获得锁的权限便可让它转为内核模式,如果线程在等待期间锁变得可用便可避免转为内核模式了。

示例代码(提供自旋、线程所有权和递归支持)
class HybridLock : IDisposable
{
    //用户模式
    private int m_Waiters = 0;

    //内核模式
    private AutoResetEvent m_WaiterLock = new AutoResetEvent(false);

    //控制自旋
    private int m_SpinCount = 1000;

    //用有锁的线程
    private int m_OwningThreadId = 0;

    //拥有次数
    private int m_Recursion = 0;

    public void Enter()
    {
        //如果线程已经拥有,递增递归次数并返回
        var threadId = Thread.CurrentThread.ManagedThreadId;
        if(this.m_OwningThreadId == threadId)
        {
            this.m_Recursion++;
            return;
        }

        //尝试获取
        var spinWait = new SpinWait();
        for (int i = 0; i < this.m_SpinCount; i++)
        {
            //如果锁可以使用了
            if (Interlocked.CompareExchange(ref this.m_Waiters, 1, 0) == 0)
                goto GotLock;

            //给其它线程运行的机会,希望锁会被释放
            spinWait.SpinOnce();
        }

        //自旋结束,仍然没有获得锁则再试一次
        if(Interlocked.Increment(ref this.m_Waiters) > 1)
        {
            //有其它线程被阻塞,这个线程也必须阻塞
            this.m_WaiterLock.WaitOne(); //性能损失
            //等待该线程用有锁醒来
        }

    GotLock:
        //一个线程用有锁时,记录ID并指出线程拥有锁一次
        this.m_OwningThreadId = threadId;
        this.m_Recursion = 1;
    }

    public void Leave()
    {
        //如果线程不用有锁,bug
        var threadId = Thread.CurrentThread.ManagedThreadId;
        if (threadId != this.m_OwningThreadId)
            throw new SynchronizationLockException("线程未拥有锁!");

        //递减递归计数,如果线程仍然用有锁,直接返回
        if (--this.m_Recursion > 0)
            return;

        //现在没有线程拥有锁
        this.m_OwningThreadId = 0;

        //若没有其它线程被阻塞直接返回
        if (Interlocked.Decrement(ref this.m_Waiters) == 0)
            return;

        //唤醒一个被阻塞的线程
        this.m_WaiterLock.Set(); //性能损失
    }

    public void Dispose()
    {
        this.m_WaiterLock.Dispose();
    }
}
Monitor类

这个类提供了一个互斥锁,这个锁支持自旋、线程所有权和递归,是线程同步常用的类。

工作方式

CLR初始化时分配一个同步块数组,一个对象在堆中创建的时候,都有两个额外的字段与它关联,第一个字段是“类型对象指针”,它包含类型对象的内存地址。第二个字段是“同步索引块”,它包含同步块数组中的一个整数索引。

一个对象在构造时,对象的同步块索引初始化为-1,表明不引用任何同步块。然后调用Monitor.Enter时,CLR在数组中找到一个空白同步块,并设置对象的同步块索引来引用该同步块。调用Exit时,会检查是否有其它任何线程正在等待使用对象的同步块,如果没有线程在等待,同步块就能自由被使用了,Exit将对象的同步块索引改回-1,自由的同步块将来可以和另一个对象关联。

堆中对象的同步块索引和CLR的同步块数组元素之间的关系图

image.png

Monitor的使用
class SomeType
{
    //对象私有锁
    private readonly object m_Lock = new object();

    public void DoSomething()
    {
        //进入私有锁
        Monitor.Enter(this.m_Lock);
        //其它代码...

        //退出私有锁
        Monitor.Exit(this.m_Lock);
    }
}
ReaderWriterLockSlim类
功能介绍
  • 一个线程写入数据时,请求访问的其它所有线程都被阻塞
  • 一个线程读取数据时,请求读取的其它线程允许继续执行,但请求写入的线程还是会被阻塞
  • 写入数据的线程结束后,可以解除一个写入线程的阻塞,使它能写入数据,也可以解除所有读取线程的阻塞,让它们并发读取数据。如果没有线程被阻塞,锁就会进入自由状态,允许下一个reader或writer线程获取
  • 读取数据的所有线程结束后,一个writer线程会被解除阻塞,使它能写入数据,如果没有线程被阻塞,锁就会进入自由状态,允许下一个reader或writer线程获取
ReaderWriterLockSlim用法
class SomeType : IDisposable
{
    private readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

    public void DoWrite()
    {
        this.m_Lock.EnterWriteLock();
        //其它代码...

        //退出私有锁
        this.m_Lock.ExitWriteLock();
    }

    public void DoRead()
    {
        this.m_Lock.EnterReadLock();
        //其它代码...

        this.m_Lock.ExitReadLock();
    }

    public void Dispose()
    {
        this.m_Lock.Dispose();
    }
}

在构造ReaderWriterLockSlim对象的时候允许传递一个LockRecursionPolicy标志,如果传递LockRecursionPolicy.SupportsRecursion,锁就可以支持线程所有权和递归行为(这些行为会对锁的性能产生负面影响),如果支持这些行为锁必须跟踪允许进入锁的所有reader线程,同时为每个线程都单独维护一个递归技术,花费更大的代价,因此建议构造的时候传入LockRecursionPolicy.NoRecursion。

双检锁(延迟初始化)
class Singleton
{
    private static object s_Lock = new object();

    private static Singleton s_Value = null;

    //私有构造
    private Singleton() { }

    public static Singleton GetSingleton()
    {
        if (s_Value != null)
            return s_Value;

        //让一个线程创建它
        Monitor.Enter(s_Lock);
        if (s_Value == null)
        {
            var temp = new Singleton();
            Interlocked.Exchange(ref s_Value, temp);
        }
        Monitor.Exit(s_Lock);
        return s_Value;
    }
}
为何使用Interlocked.Exchange

为何不直接s_Value=new Singleton(),原因在于这种写法编译器可能先为Singleton分配内存,再将引用赋值给s_Value,最后调用构造器。从单线程来看这样并没什么问题,如果是多线程,此时在引用已经赋值给s_Value了,然而却还未调用构造器,这时另一个线程调用了GetSingleton方法,发现s_Value不为null,所以开始使用Singleton对象,但是对象的构造器还未执行结束,产生bug。而使用Interlocked.Exchange可以修正该问题,方法保证temp中的引用只有在构造器执行结束后才赋值给s_Value。

Monitor类的Wait与Pulse

如果一个线程希望一个复合条件为true时执行一些代码,便可以使用Wait与Pulse,在条件不满足的时候Wait,另一个线程更改条件后Pulse,而非让线程自旋连续检测条件,浪费CPU时间。

示例代码
class ConditionVariablePattern
{
    private readonly object m_Lock = new object();
    private bool m_Condition = false;

    public void Thread1()
    {
        //获取一个互斥锁
        Monitor.Enter(this.m_Lock);

        //在锁中原子性地检测复合条件
        while (!this.m_Condition)
        {
            //条件不满足,等待其它线程修改
            Monitor.Wait(this.m_Lock);
        }

        //条件满足,处理数据...
        Monitor.Exit(this.m_Lock);
    }

    public void Thread2()
    {
        //获取一个互斥锁
        Monitor.Enter(this.m_Lock);

        //处理数据并修改条件
        this.m_Condition = true;

        //Monitor.Pulse(this.m_Lock);     //释放之后唤醒一个正在等待的线程
        Monitor.PulseAll(this.m_Lock);  //释放之后唤醒所有正在等待的线程

        Monitor.Exit(this.m_Lock);
    }
}
代码解析

Thread1获取一个互斥锁,然后对一个条件变量进行检测,如果条件不满足,调用Monitor.Wait释放锁并阻塞调用线程,其它线程能获得改锁。

Thread2获取锁的所有权,处理数据,造成一些状态的改变,其中包括Thread1要检测的条件变量,然后调用Monitor.PulseAll或者Monitor.Pulse,从而解除一个因为调用Wait方法而进入阻塞的线程。

其中Pulse只解除等待最久的线程的阻塞,PulseAll解除所有等待线程的阻塞,然而解除阻塞的线程还必须等待Thread2线程调用完Exit才能拥有锁。

Thread1醒来时,进行下一次循环迭代,再次对条件进行检测,如过条件仍为false,继续调用Wait。如果条件满足,处理一些数据,最后调用Exit永久释放锁。

BlockingCollection类实现生产者/消费者模式
static void Main()
{
    //BlockingCollection类实现生产者/消费者模式
    var bl = new BlockingCollection<int>(new ConcurrentQueue<int>());

    //由一个线程池执行消费
    ThreadPool.QueueUserWorkItem(ConsumeItems, bl);

    //在集合中添加数据项
    for (int i = 0; i < 6; i++)
    {
        Console.WriteLine("production item : {0}", i);
        bl.Add(i);
    }

    //通知消费线程不会在集合中添加更多的item了
    bl.CompleteAdding();

    Console.ReadKey();
}
运行结果

image.png

生产与消费行为可能出现交错

image.png

查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 2月20日

线程安全

线程安全

多个线程试图同时访问同一个数据时,数据不会遭到破坏

线程同步构造

构造模式分别有用户模式和内核模式两种,其中用户模式构造使用了特殊的CPU指令协调线程(协调是在硬件中发生的事情),所以其构造速度要显著快于内核模式构造,同时用户模式中阻塞的线程池线程永远不会被认为阻塞,所以线程池不会创建新线程替换阻塞线程。在用户模式中运行的线程可能被系统抢占,但线程会以最快的速度再次调度,所以想要获取某一资源又暂时无法取得时,线程会用户模式中一直运行,这并不是一个良好的现象。而内核模式的构造是由Windows操作系统自身提供的,要求在应用程序的线程中调用在操作系统内核中实现的函数,将线程从用户模式切换为内核模式会造成巨大的性能损失。但是也有一个优点:一个线程使用内核模式构造获取一个由其它线程正在访问的资源时,Windows会阻塞线程,使之不再浪费CPU时间,等到资源可用时会恢复线程,允许它访问资源。

用户模式构造
  • 易失构造:在包含一个简单数据类型的变量上执行原子性的读或写操作
  • 互锁构造:在包含一个简单数据类型的变量上执行原子性的读和写操作
原子性

指事务的不可分割性,意味着一个变量的值的读取都是一次性的,如以下代码

class SomeType
{
    public static int x;
}

SomeType.x = 0x01234567;

变量x会一次性从0x00000000变成0x01234567,另一个线程不可能看到一个处于中间值的状态,如0x01234000,这便是原子性。

易失构造

编写好的代码需要被编译器编译成IL代码,再经过JIT编译器转换成本地CPU指令才能被计算机执行。而在这些转换过程中,编译器、JIT编译器、CPU本身可能都会对原先编写好的代码进行优化。如下面这段代码经过编译后将会消失

private static void SomeMethod()
{
    //常量表达式在编译时计算为0
    int value = 100 - (50 * 2);
    //value为0循环永不执行
    for (int i = 0; i < value; i++)
    {
        //永远执行不到,不需要编译循环中的代码
        Console.WriteLine(i);
    }
}

上述代码中,编译器发现value为0,循环永远不会执行,没有必要编译循环中的代码,因此这个方法编译后会被优化掉。如果有一个方法中调用了SomeMethod方法,在对这个方法进行JIT编译的时候,JIT编译器会尝试内联SomeMethod方法的代码,由于没有代码,所以JIT编译器会删除调用SomeMethod方法的代码。

编译器、JIT编译器和CPU对代码进行优化的时候,从单线程的角度看,代码会做我们希望它做的事情,而从多线程来看,代码的意图不一定会得到保留,以下的代码进行了演示:

class SomeType
{
    private int m_Flag = 0;
    private int m_Value = 0;

    public void Thread1()
    {
        this.m_Value = 10;
        this.m_Flag = 1;
    }

    public void Thread2()
    {
        //可能会输出0,与预期不一致
        if(this.m_Flag == 1)
            Console.WriteLine("value = {0}", this.m_Value);
    }
}

static void Main()
{
    ThreadPool.QueueUserWorkItem((o) =>
    {
        someType.Thread1();
    });
    ThreadPool.QueueUserWorkItem((o) =>
    {
        someType.Thread2();
    });
}

上述代码的问题在于假定Thread1方法中的代码按照顺序执行,编译Thread2方法中的代码时,编译器必须生成代码将m_Flag和m_Value 从RAM读入CPU寄存器。RAM可能先传递m_Value的值(此时为0),然后Thread1可能执行,将Thread1改为10,m_Flag改为1。但是Thread2的CPU寄存器没有看到m_Value的值已经被另一个线程修改为10,出现输出结果为0的情况。除此之外Thread1方法中的两行代码在CUP/编译器在解释代码时可能会出现反转,毕竟这样做也不会改变代码的意图,同样可能出现在Thread2中m_Value输出0的情况。

修改代码以修复问题,修改后的代码如下:
class SomeType
{
    private int m_Flag = 0;
    private int m_Value = 0;

    public void Thread1()
    {
        this.m_Value = 10;
        Thread.VolatileWrite(ref this.m_Flag, 1);
    }

    public void Thread2()
    {
        if (Thread.VolatileRead(ref this.m_Flag) == 1)
            Console.WriteLine("value = {0}", this.m_Value);
    }
}

修改后的代码可以看到分别使用了VolatileWrite和VolatileRead来读写数据,Thread1方法调用VolatileWrite可以确保前面的所有数据都写入完成才会将1写入m_Flag;Thread2方法调用VolatileRead可以确保必须先读取m_Flag的值才能读取m_Value的值。

VolatileWrite和VolatileRead
  • VolatileWrite:强迫address中的值在调用时写入,除此之外还必须按照顺序,即所有发生在VolatileWrite之前的加载和存储操作必须先于调用VolatileWrite方法完成
  • VolatileRead:强迫address中的值在调用时读取,除此之外还必须按照顺序,即所有发生在VolatileRead之后的加载和存储操作必须晚于调用VolatileRead方法完成
volatile关键字
class SomeType
{
    private volatile int m_Flag = 0;
    private int m_Value = 0;

    public void Thread1()
    {
        this.m_Value = 10;
        this.m_Flag = 1;
    }

    public void Thread2()
    {
        if (this.m_Flag == 1)
            Console.WriteLine("value = {0}", this.m_Value);
    }
}

使用volatile关键字可以达到和调用VolatileWrite和VolatileRead相同的效果,除此之外volatile关键字告诉C#和JIT编译器不将字段缓存到CPU寄存器中,确保字段的所有读写都在RAM中进行。

调用VolatileWrite方法或VolatileRead方法、使用volatile关键字将会禁用C#编译器、JIT编译器和CPU本身所执行的一些代码优化,如果使用不当反而会损害性能。并且C#不支持以传引用的方式将volatile修饰的字段传递给方法。

自旋锁
struct SpinLock
{
    private int m_ResourceInUse;

    public void Enter()
    {
        //将资源设置为正在使用,并返回m_ResourceInUse的原始值
        while (Interlocked.Exchange(ref this.m_ResourceInUse, 1) != 0) { }
    }

    public void Leave()
    {
        //释放资源
        Thread.VolatileWrite(ref this.m_ResourceInUse, 0);
    }
}

private static SpinLock s_SpinLock = new SpinLock();
private static void DoSomething()
{
    s_SpinLock.Enter();
    //一次只有一个线程才能进入这里执行代码
    s_SpinLock.Leave();
}

现在如果两个线程同时调用Enter,Interlocked.Exchange会确保其中一个线程将m_ResourceInUse从0变到1,并返回m_ResourceInUse的原始值0,然后线程从Enter返回,继续执行后面的代码。另一个线程会将m_ResourceInUse从1变到1,并返回原始值1,发现不是将m_ResourceInUse从0变成1的,所以会一直调用Interlocked.Exchange开始自旋,直到第一个线程调用Leave。第一个线程调用Leave后,会将m_ResourceInUse重新变成0,这时正在自旋的线程调用Interlocked.Exchange能够将m_ResourceInUse从0变成1,于是从Enter返回继续执行后续的代码。

自旋锁的缺点在于处于自旋的线程无法做其它的工作,浪费CPU时间,建议只将自旋锁用于保护执行得非常快的代码块。

内核构造构造
内核模式构造的缺点

由于需要Windows操作系统的自身协作以及内核对象上调用的每个方法都会造成调用线程从托管代码转换成本地用户代码,再转换为本地内核模式代码,这些转换需要大量的CPU时间,如果经常执行可能会对应用程序的性能造成负面影响。

内核模式构造的优点
  • 在资源竞争时,Windows会阻塞输掉的线程,让它不占用CPU从而浪费处理器资源
  • 在内核模式构造上阻塞的线程可以指定超时值,如果指定时间内访问不到希望得到的资源,线程可以解除阻塞执行其它任务
  • 一个线程可以一直阻塞,直到一个集合中的所有内核模式的构造皆可使用或者一个集合中的任何内核模式的构造可用
通过内核构造实现一个单实例应用程序
static void Main()
{
    bool createdNew;
    //创建一个具有指定名称的内核对象
    using (new Semaphore(0, 1, "MyObject", out createdNew))
    {
        if (createdNew)
        {
            //线程创建了内核对象,所以肯定没有这个应用程序的其它实例正在运行
        }
        else
        {
            //线程打开了一个现有的内核对象,说明实例正在被使用,立即退出
        }
    }
}
代码解析

假设进程的两个实例同时启动。每个进程都有自己的线程,两个线程都尝试创建具有相同字符串名称“MyObject”的一个Semaphore。Windows内核确保只有一个线程创建具有指定名称的内核对象。创建对象的线程会将它的createdNew设置为true。

第二个线程,Windows发现具有指定名称的内核对象已经存在了,因此不允许第二个线程创建另一个同名的内核对象,但是却可以访问和第一个进程的线程所访问的一样的内核对象。不同进程的线程便是这样通过一个内核对象互相通信的。在上述代码中第二个线程发现createdNew变量为false,所以知道这个进程的另一个实例正在运行,所以进程的第二个实例立即退出。

Event构造

事件是由内核维护的Boolean变量,如果事件为false,在事件上等待的线程就阻塞,反之解除阻塞。事件分为自动重置事件和手动重置事件,当自动重置事件为true时,只唤醒一个阻塞的线程,因为在解除第一个线程的阻塞后,内核将事件重置回false。当手动重置事件为true时,会解除正在等待的所有线程的阻塞,因为内核不将事件自动重置为false,代码必须将事件手动重置回false。

使用自动同步事件创建线程同步锁
class WaitLock : IDisposable
{ 
    private AutoResetEvent m_Resources = new AutoResetEvent(true);

    public void Enter()
    {
        //在内核中阻塞,等待资源可用然后返回
        this.m_Resources.WaitOne();
    }

    public void Leave()
    {
        //释放资源
        this.m_Resources.Set();
    }

    public void Dispose()
    {
        this.m_Resources.Dispose();
    }
}
SpinLock与WaitLock性能对比
static void Method() { }

static void Main()
{
    var x = 0;
    var iteration = 10000000;

    //x递增1000万需要花费时间
    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < iteration; i++)
        x++;
    Console.WriteLine("x递增1000万次花费时间: {0}", sw.ElapsedMilliseconds);

    //x递增1000万次加上调用一个空方法需要花费的时间
    sw.Restart();
    for (int i = 0; i < iteration; i++)
    {
        Method();
        x++;
    }
    Console.WriteLine("x递增1000万次加上调用一个空方法需要花费的时间: {0}", sw.ElapsedMilliseconds);

    //x递增1000万次加上一个无竞争的SpinLock需要花费的时间
    SpinLock spinLock = new SpinLock();
    sw.Restart();
    for (int i = 0; i < iteration; i++)
    {
        spinLock.Enter();
        x++;
        spinLock.Leave();
    }
    Console.WriteLine("x递增1000万次加上一个无竞争的SpinLock需要花费的时间: {0}", sw.ElapsedMilliseconds);

    //x递增1000万次加上一个无竞争的WaitLock需要花费的时间
    using (var waitLock = new WaitLock())
    {
        sw.Restart();
        for (int i = 0; i < iteration; i++)
        {
            waitLock.Enter();
            x++;
            waitLock.Leave();
        }
        Console.WriteLine("x递增1000万次加上一个无竞争的WaitLock需要花费的时间: {0}", sw.ElapsedMilliseconds);
    }

    Console.ReadKey();
}
运行结果

image.png

可以看出SpinLock和WaitLock的行为完全相同,但是两个锁的性能完全不同。锁上面没有竞争的时候WaitLock比SpinLock慢得多,因为上面说到的WaitLock的Enter和Leave方法的每一次调用都强迫调用线程从托管代码转换成内核代码。但在存在竞争的时候,输掉的线程会被内核阻塞,不会造成自旋,这是好的地方。

通过例子可以看出内核构造速度慢得可怕,所以需要进行线程同步的时候尽量使用用户模式的构造。

Semaphore构造

信号量(Semaphore)是由内核维护的Int32变量,信号量为0时,在信号量上等待的线程会阻塞。信号量大于0时,就会解除阻塞。在一个信号量上等待的一个线程解除阻塞时,内核自动从信号量的计数中减1。当前信号量计数不能超过信号量关联的最大计数值。

Event构造与Semaphore构造对比
  • 自动重置事件:多个线程在一个自动重置事件上等待时,设置事件只导致一个线程被解除阻塞
  • 手动重置事件:多个线程在一个手动重置事件上等待时,设置事件会导致所有线程被解除阻塞
  • Semaphore构造:多个线程在一个信号量上等待时,释放信号量导致导致releaseCount(释放信号量个数)个线程被解除阻塞(releaseCount是传给Semaphore的Release方法的实参)

一个自动重置事件在行为上和最大计数为1的信号量非常相似,两者的区别就在,可以在一个自动重置事件上连续多次调用Set,同时仍然只有一个线程被解除阻塞。而在一个信号量上连续多次调用Release,会使它内部的计数一直递增,这可能造成解除大量线程的阻塞。而当计数超过最大计数时,Release会抛出SemaphoreFullException。

示例代码
class SemaphoreLock : IDisposable
{
    private Semaphore m_Resources;

    public SemaphoreLock(int coumaximumConcurThreads)
    {
        this.m_Resources = new Semaphore(coumaximumConcurThreads, coumaximumConcurThreads);
    }

    public void Enter()
    {
        //在内核中阻塞,等待资源可用然后返回
        this.m_Resources.WaitOne();
    }

    public void Leave()
    {
        //释放资源
        this.m_Resources.Release();
    }

    public void Dispose()
    {
        this.m_Resources.Close();
    }
}
Event实现交替执行的两个线程
private static int Index = 0;
private static AutoResetEvent s_EventOdd = new AutoResetEvent(false);
private static AutoResetEvent s_EventEven = new AutoResetEvent(false);

//输出奇数
static void OutputOdd()
{
    while (true)
    {
        s_EventOdd.WaitOne();
        Index++;
        Console.WriteLine("Thread1 Index : {0}", Index);
        Thread.Sleep(500);
        s_EventEven.Set();
    }
}

//输出偶数
static void OutputEven()
{
    while (true)
    {
        s_EventEven.WaitOne();
        Index++;
        Console.WriteLine("Thread2 Index : {0}", Index);
        Thread.Sleep(500);
        s_EventOdd.Set();
    }
}

static void Main()
{
    //线程交替执行
    new Action(OutputOdd).BeginInvoke(null, null);
    new Action(OutputEven).BeginInvoke(null, null);
    s_EventOdd.Set();

    Console.ReadKey();
}
运行结果

image.png

Mutex(互斥锁)构造

互斥锁的逻辑
首先Mutex对象会查询调用线程的int ID,记录是哪一个线程获得了锁。一个线程调用ReleaseMutex时,Mutex确保调用线程就是获取Mutex的那个线程。如果不是,Mutex对象的状态就不会改变,同时ReleaseMutex也会抛出异常ApplicationException。

其次如果拥有Mutex的线程终止,那么Mutex上等待的一些线程会因为抛出一个AbandonedMutexException异常而被唤醒,通常该异常也会成为未处理异常。

Mutex对象还维护着一个递归计数,它指明拥有该Mutex的线程拥有了它多少次。如果一个线程当前拥有一个Mutex,然后该线程再次在Mutex上等待,递归计数将递增,且不会阻塞线程,允许这个线程继续执行。线程调用ReleaseMutex时,递归计数递减。只有在递归计数变成0时,另一个线程才能获取该Mutex。

Mutex的缺点

需要更多的内存容纳额外的线程ID和递归计数信息,Mutex代码还得维护这些信息,这些都会让锁变得更慢。

递归锁
class SomeType : IDisposable
{
    private readonly Mutex m_Lock = new Mutex();

    public void M1()
    {
        this.m_Lock.WaitOne();
        //do something...
        M2(); //递归获取锁
        this.m_Lock.ReleaseMutex();
    }

    public void M2()
    {
        this.m_Lock.WaitOne();
        //do something...
        this.m_Lock.ReleaseMutex();
    }

    public void Dispose()
    {
        this.m_Lock.Dispose();
    }
}

SomeType对象调用M1获取一个Mutex,然后调用M2,由于Mutex对象支持递归,所以线程会获取两次锁,然后释放两次,之后另一个线程才能拥有它。

内核构造可用时回调方法

让一个线程不确定地等待一个内核对象进入可用状态,这对线程的内存资源来说是一种浪费,因此线程池提供了一种方式,在一个内核对象变得可用时回调一个方法。

示例代码
class RegisterdWaitHandleClass
{
    public static void Main()
    {
        //构造自动重置事件
        AutoResetEvent autoResetEvent = new AutoResetEvent(false);

        //告诉线程池在AutoResetEvent上等待
        RegisteredWaitHandle rwh = ThreadPool.RegisterWaitForSingleObject(
            autoResetEvent, //在此事件上等待
            EventOperation, //回调EventOperation方法
            null, //向EventOperation传递null
            5000, //等5s事件变为True
            false //每次事件变为True时都调用EventOperation
        );

        var operation = (char)0;
        while(operation != 'Q')
        {
            operation = char.ToUpper(Console.ReadKey(true).KeyChar);
            if (operation == 'S')
                autoResetEvent.Set();
        }

        //取消注册
        rwh.Unregister(null);
    }

    //任何时候事件为True,或者自从上一次回调超过5s,就调用这个方法
    private static void EventOperation(object state, bool timedOut)
    {
        Console.WriteLine(timedOut ? "超时" : "事件为True");
    }
}
运行结果(每隔5s输出超时,键盘按下S输出事件为True)

image.png

查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 2月6日

线程Ⅱ

线程池
为什么要使用线程池?

创建和销毁线程是一个昂贵的操作,要耗费大量的时间。太多的线程还会浪费内存资源,由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还会影响性能,而线程池可以改善这些情况。

线程池是什么?

可以将线程池想象成为可以由应用程序使用的一个线程集合,每个CLR一个线程池,这个线程池由CLR控制的所有AppDomain共享。如果一个进程中加载了多个CLR,那么每个CLR都有自己的线程池。

线程池是如何工作的?

CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列,在应用程序想执行一个异步操作时,就调用一个方法,将一个记录项追加到线程池的队列中。线程池的代码从这个队列中提取记录项,将这个记录项派遣给一个线程池线程。此时如果线程池中没有线程,会创建一个新的线程(产生一定的性能开销)。当线程完成任务后,线程不会被销毁,而会回到线程池且进入空闲状态,等待响应下一个请求。由于线程不再销毁所以不再产生额外的性能损失。

如果应用程序向线程发出许多请求,线程池会尝试只用已有一个线程处理这些请求。如果发送请求的速度超过了线程池线程处理的速度,就会创建额外的线程来处理。

当应用程序停止向线程池发送求情,此时线程池中的线程什么都不做,造成内存资源的浪费。所以有一个机制:当线程池线程闲置一段时间后(不同版本的CLR对时间有所差异),线程会自动唤醒并终止自己释放资源。

特点

线程池可以容纳少量线程避免资源浪费,也可以创建大量线程充分利用多处理器、超线程处理器以及多核处理器。换句话说线程池是启发式的,如果应用程序要执行许多任务同时又有可用的CPU,那么线程池会创建更多的线程。

使用线程池异步编程
private static void SomeMethod(object state)
{
    //方法由线程池线程执行
    Console.WriteLine("state = {0}", state);
    Thread.Sleep(10000);

    //方法返回后线程回到线程池等待下一个请求
}

static void Main()
{
    ThreadPool.QueueUserWorkItem(SomeMethod, 1);
    Console.ReadKey();
}
执行上下文

每个线程都关联了一个执行上下文数据结构,包含有:安全设置、宿主设置以及逻辑调用上下文数据。正常情况下,每当一个线程(初始线程)使用另一个线程(辅助线程)执行任务时,前者的执行上下文应该复制到辅助线程,这样可以确保辅助线程的任何操作都是使用同样的安全设置和宿主设置,还能保证初始线程的逻辑调用上下文可以在辅助线程中使用。默认情况下初始线程的执行上下文可以流向任何辅助线程,但执行上下文中包含大量信息,这会对性能造成一定的影响。

ExecutionContext控制执行上下文
static void Main(string[] args)
{
    //将数据放入Main线程的逻辑调用上下文
    CallContext.LogicalSetData("Name", "DoubleJ");

    //线程池线程能访问逻辑调用上下文数据
    ThreadPool.QueueUserWorkItem(state => {
        Console.WriteLine("state = {0}, name = {1}", state, CallContext.LogicalGetData("Name"));
    }, 1);

    //阻止Main线程的执行上下文流动
    ExecutionContext.SuppressFlow();

    //线程池线程将无法访问逻辑调用上下文数据
    ThreadPool.QueueUserWorkItem(state => {
        Console.WriteLine("state = {0}, name = {1}", state, CallContext.LogicalGetData("Name"));
    }, 2);

    //恢复Main线程的执行上下文流动
    ExecutionContext.RestoreFlow();

    //线程池线程又可以访问逻辑调用上下文数据了
    ThreadPool.QueueUserWorkItem(state => {
        Console.WriteLine("state = {0}, name = {1}", state, CallContext.LogicalGetData("Name"));
    }, 3);

    Console.ReadKey();
}
运行结果

image.png

任务

ThreadPool的QueueUserWorkItem方法虽然非常简单,但是却没办法知道操作在什么时候完成以及获取返回值。现在使用任务(task)可以弥补这些不足。

等待任务完成并获取返回结果
static void Main()
{
    Task<int> t = new Task<int>(() =>
    {
        int sum = 0;
        for (int i = 0; i < 10000; i++)
            sum += i;
        return sum;
    });

    //启动任务
    t.Start();

    //等待任务完成
    t.Wait();

    //查看返回结果
    Console.WriteLine("result = {0}", t.Result);
    Console.ReadKey();
}

线程调用Wait方法时,系统会检测线程要等待的Task是否已经开始执行,如果是,调用Wait方法的线程会阻塞,直到Task运行结束为止。如果Task还没有开始执行,系统可能会使用调用Wait方法的线程来执行Task,这种情况调用Wait的线程将不会阻塞,它会执行Task并立即返回。如果线程在调用Wait前已经获得了一个线程同步锁,而Task试图获取同一个锁,就会造成线程死锁。

取消任务
static void Main()
{
    var t = new Task<int>(() =>
    {
        int sum = 0;
        for (int i = 0; i < 10000; i++)
        {
            //如果已取消则会抛出异常
            cts.Token.ThrowIfCancellationRequested();
            sum += i;
        }
        return sum;
    }, cts.Token);

    t.Start();
    //异步请求,Task可能已经完成了
    cts.Cancel();

    try
    {
        //如果任务已取消,Result会引发AggregateException
        Console.WriteLine("result = {0}", t.Result);
    }
    catch (AggregateException exp)
    {
        exp.Handle(e => e is OperationCanceledException);
        Console.WriteLine("任务已取消");
    }
    Console.ReadKey();
}

创建一个任务时可以将一个CancellationToken传给Task的构造器,从而将CancellationToken和该Task关联在一起。如果CancellationToken在Task调度前被取消,则Task永远都不会再执行。

运行结果

image.png

一个任务完成时自动启动一个新的任务
private static int Sum(int n)
{
    n += 1;
    Console.WriteLine("n = {0}", n);
    return n;
}

static void Main()
{
    Task<int> t = new Task<int>(n => Sum((int)n), 0);
    t.Start();
    t.ContinueWith(task => Sum(task.Result));
    Console.ReadKey();
}

上述代码执行完任务(t)时,会启动另一个任务,执行上述代码的线程不会进入阻塞状态并等待两个任务中的任意一个任务完成,线程可以继续执行其它代码。

运行结果

image.png

父任务和子任务
private static int Sum(int n)
{
    n += 1;
    Console.WriteLine("n = {0}", n);
    return n;
}

static void Main()
{
    Task<int[]> parent = new Task<int[]>(() =>
    {
        var result = new int[3];
        new Task(() => result[0] = Sum(0), TaskCreationOptions.AttachedToParent).Start();
        new Task(() => { Thread.Sleep(5000); result[1] = Sum(1); }, TaskCreationOptions.AttachedToParent).Start();
        new Task(() => result[2] = Sum(2), TaskCreationOptions.AttachedToParent).Start();
        return result;
    });
    parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
    parent.Start();
    Console.ReadKey();
}
运行结果

image.png

现在改动一行代码,如下:

new Task(() => { Thread.Sleep(5000); result[1] = Sum(1); }, TaskCreationOptions.AttachedToParent).Start();

//上段代码改为
new Task(() => { Thread.Sleep(5000); result[1] = Sum(1); }).Start();
运行结果

image.png

结论
默认情况一个任务创建的Task对象是顶级任务,这些任务与创建他们的那个任务没有关联,然而使用TaskCreationOptions.AttachedToParent标记将一个Task和创建它的那个Task关联起来,这样一来除非所有的子任务以及子任务的子任务结束运行,否则父任务就不会认为已经结束。

任务工厂
private static int Sum(int n)
{
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        checked
        {
            sum += i;
        }
    }
    return sum;
}

static void Main()
{
    Task parent = new Task(() => {
        var cts = new CancellationTokenSource();
        var tf = new TaskFactory<int>(
            cts.Token,
            TaskCreationOptions.AttachedToParent,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default
        );

        //创建并启动子任务
        var childTask = new[]
        {
            tf.StartNew(() => Sum(1000)),
            tf.StartNew(() => Sum(10000)),
            tf.StartNew(() => Sum(100000))
        };

        //任何子任务抛出异常就取消其余子任务
        for (int i = 0; i < childTask.Length; i++)
            childTask[i].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);

        //完成所有子任务后
        tf.ContinueWhenAll(
            childTask,
            completedTask => completedTask.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result),
            CancellationToken.None
        ).ContinueWith(
            t => Console.WriteLine("max result is : {0}", t.Result
        ), TaskContinuationOptions.ExecuteSynchronously);
    });

    //子任务完成后
    parent.ContinueWith(p =>
    {
        foreach (var e in p.Exception.Flatten().InnerExceptions)
            Console.WriteLine("Exception : {0}", e.Message);
    }, TaskContinuationOptions.OnlyOnFaulted);
    parent.Start();
    Console.ReadKey();
}
运行结果

image.png

Parallel类的使用
Parallel的静态For方法
//单线程执行(不建议)
for (int i = 0; i < 100000; i++)
    DoSomthing(i);

//并行工作(推荐)
Parallel.For(0, 100000, (i) => DoSomthing(i));
Parallel的静态ForEach方法
var collection = new int[] { 1, 2, 3 };
//不建议
foreach (var item in collection)
    DoSomthing(item);

//推荐
Parallel.ForEach(collection, item => DoSomthing(item));

*如果既可以使用For也可以使用ForEach,那么使用For方法的速度要比ForEach方法快一些

Parallel的静态Invoke方法
//一个线程顺序执行多个方法
Method1();
Method2();
Method3();

//线程池的线程并行执行方法
Parallel.Invoke(
    () => Method1(),
    () => Method2(),
    () => Method3()
);

使用Parallel的前提是代码必须可以是并行执行的,如果任务一定要顺序执行请勿使用。Parallel的所有方法都让调用线程参与处理,如果调用线程在线程池线程完成自己的那一部分工作之前完成工作,那么调用线程便会自动挂起,直到所有工作完成后继续。

注意事项

Parallel虽然好用,但也需要开销,其中必须分配委托对象,而每一个工作项,都要调用一次委托。如果有大量可以使用多线程处理的工作项,那么也许能提升性能。再或者每一个工作项都需要涉及大量的工作,那么调用委托所造成的性能损失便可以忽略不计。但是如果工作项很少或者每一个工作项都能处理得非常快,这种情况下使用Parallel的方法反而会损害性能。

For,ForEach,Invoke的参数ParallelOptions对象
  • CancellationToken:允许取消操作,默认为CancellationToken.None
  • MaxDegreeOfParallelism:指定可以并发执行的最大工作项数量
  • TaskScheduler:指定要使用哪个TaskScheduler,默认为TaskScheduler.Default
定时执行任务

以下代码创建了一个计时器,并且立即执行一次SomeMethod方法,之后每间隔1s继续执行SomeMethod方法

private static  System.Threading.Timer s_Timer;

static void Main()
{
    s_Timer = new Timer(SomeMethod, 6, 0, Timeout.Infinite);
    Console.ReadKey();
}

private static void SomeMethod(object state)
{
    Console.WriteLine("state = {0}", state);

    //让Timer在1s后再调用这个方法
    s_Timer.Change(1000, Timeout.Infinite);
}

在内部,线程池为所有Timer对象只使用了一个线程。该线程知道下一个Timer对象还需要多久触发,下一个Timer对象触发时线程就会唤醒,在内部调用ThreadPool的QueueUserWorkItem,将一个工作项添加到线程池的请求队列中,使回调方法可以得到调用。如果回调方法执行时间很长,计时器可能再次触发,这种情况可能造成多个线程池线程同时执行回调方法。解决这个问题可以在构造Timer对象时,将period参数指定为Timeout.Infinite,这样计时器就只会触发一次。如果要循环执行计时器可以在回调方法内部调用Change方法,如上述代码那样。

运行结果

可以看到控制台立即输出state = 6之后每隔1s在输出state = 6

不推荐的做法
private static  System.Threading.Timer s_Timer;

static void Main()
{
    s_Timer = new Timer(SomeMethod, 6, 0, 1000);
    Console.ReadKey();
}

private static void SomeMethod(object state)
{
    Console.WriteLine("state = {0}", state);
}

这种做法虽然与上一段代码结果一致,一旦回调方法执行时间过长,超过period参数指定的调用回调方法的时间间隔,那么便可能出现多个线程同时执行回调方法,这并不是想要的结果。

CLR线程池
数据结构图

image.png

管理工作者线程

ThreadPool.QueueUserWorkItem和Timer类总是将工作项放到全局队列中,工作者线程采用先入先出的算法将工作项从队列中取出,并处理它们。由于多个工作者线程可能同时从全局队列中取出工作项,所以所有工作者线程都竞争同一个线程同步锁。

每个工作者线程都有一个自己的本地队列,当一个工作者线程调度一个Task时,Task会添加到调用线程的本地队列。工作者线程准备处理一个工作项时,它总是会先检查它的本地队列,如果存在一个Task,工作者线程就从它的本地队列中移除Task,并对工作项进行处理(工作者线程采用的是后入先出的算法取出队列中的工作项)。由于每个工作者线程的本地队列只有自己能访问,所以无需线程同步锁。

当工作者线程发现自己的本地队列为空时,工作者线程就会尝试从另一个工作者线程的本地队列的尾部取出一个工作项,并要求获取一个线程同步锁(对性能有些许影响)。如果所有本地队列都为空,那么工作者线程会使用先入先出算法尝试从全局队列取出工作项并获取线程同步锁,如果全局队列也为空,那么工作者线程将进入睡眠状态,等待事情的发生。如果睡眠时间太长,将自动唤醒并销毁自身。

线程池会快速创建工作者线程,使数量等于调用ThreadPool.SetMinThreads方法传递的值,如果没有调用该方法,默认等于进程允许使用的CPU数量。通常进程允许使用机器上的所有CPU数,所以线程池创建的工作者线程数量很快便会达到机器上的CPU数量。创建线程后,线程池会监视工作项的完成速度,如果时间太长,线程池会创建更多的线程,如果工作项完成速度很快,工作者线程就会被销毁。

CPU缓存行和伪共享
缓存行

为了提升访问内存的速度,CPU在逻辑上将所有内存都划分为缓存行,缓存行是2的整数幂个连续字节,最常见的缓存行大小是64个字节,所以CPU从RAM中获取并存储64个字节块。例如应用程序读取一个Int32的数据,那么会获取包含这个Int32值的64个字节。获取更多的字节通常可以增强性能,因为应用程序大多数在访问一些数据之后继续访问这些数据周围的其它数据。此时由于相邻的数据已经在CPU的缓存中了,就避免了慢速度的RAM访问。

但是,如果两个或多个内核访问同一个缓存行中的字节,内核之间必须互相通信,并在不同的内核之间传递缓存行,造成多个内核不能同时处理相邻的字节,从而对性能造成严重的影响。

代码测试
private const int COUNT = 100000000;

private static int s_OperationCount = 2;
private static long s_StartTime;

class SomeType
{
    public int Field1;
    public int Field2;
}

private static void AccessField(SomeType type, int field)
{
    //线程各自访问type中的字段
    for (int i = 0; i < COUNT; i++)
    {
        if (field == 0)
            type.Field1++;
        else
            type.Field2++;
    }

    //最后一个线程结束后显示花费时间
    if(Interlocked.Decrement(ref s_OperationCount) == 0)
        Console.WriteLine("花费时间:{0}", (Stopwatch.GetTimestamp() - s_StartTime) / (Stopwatch.Frequency / 1000));
}

static void Main()
{
    var type = new SomeType();
    s_StartTime = Stopwatch.GetTimestamp();

    //两个线程访问对象中的字段
    ThreadPool.QueueUserWorkItem(o => AccessField(type, 0));
    ThreadPool.QueueUserWorkItem(o => AccessField(type, 1));

    Console.ReadKey();
}

上述代码的type对象包含两个字段Field1和Field2,这两个字段极有可能在同一个缓存行中,接着启动两个线程执行AccessField方法,一个线程操作Field1,另一个线程操作Field2,每个线程完成时递减s_OperationCount的值,最后显示两个线程完成工作花费的总时间。

运行结果

image.png

接着修改一下SomeType类,让它变成这样:

[StructLayout(LayoutKind.Explicit)]
class SomeType
{
    [FieldOffset(0)]
    public int Field1;

    [FieldOffset(64)]
    public int Field2;
}

修改后的SomeType类使用了缓存行分隔Field1字段和Field2字段,在第一个版本中这个两个字段属于同一个缓存行,造成不同的CPU必须不停的来回传递字节。虽然从程序的角度看,两个线程处理的是不同的数据,但从CPU的缓存行角度看,CPU处理的是相同的数据,称为伪共享。在修改后的代码中,字段分别属于不同的缓存行,所以CPU可以做到独立工作,不必共享。

再次执行查看结果,速度显著提升

image.png

访问数组

由于数组在数组内存起始处维护着数组的长度信息,具体位置是在前几个元素之后。访问一个元素时,CLR会验证使用的索引是否在数组的长度范围内。所以访问一个数组的元素总是会牵扯到访问数组的长度,因此为了避免产生额外的伪共享,应避免让一个线程访问数组的前几个元素,同时让另一个线程访问数组中的其它元素。

查看原文

赞 0 收藏 0 评论 0

DoubleJ 发布了文章 · 2月3日

线程

什么是线程?

可以将线程理解成逻辑CPU,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程开销
  • 线程内核对象:OS为系统中创建的每个线程都会分配并初始化这种数据结构。在这个数据结构中,包含一组对线程进行描述的属性,还包含线程上下文。上下文是一个内存块,包含了CPU的寄存器集合。Windows在使用x 86CPU机器上运行时,线程上下文使用约700字节的内存,而对于x64 CPU上下文使用约1240字节内存。
  • 线程环境块:是在应用程序代码能快速访问的地址空间中分配和初始化的一个内存块。线程环境块耗用一个内存页(x86和x64都是4KB),线程环境块还包含线程的异常处理链首,线程进入的每个try块都在链首插入一个节点。线程退出try块时,会从链中删除该节点。除此之外线程环境块还包含线程本地存储数据以及GDI(图形设备接口)和OpenGL图形使用的一些数据结构。
  • 用户模式栈:用于存储传给方法的局部变量和实参。还包含一个地址,该地址指出当前方法返回时,线程接下去应该从什么地方开始执行,默认情况下Windows为每个用户模式栈分配1MB内存。
  • 内核模式栈:应用程序代码向操作系统中的一个内核模式的函数传递实参时,还会使用内核模式栈。出于安全考虑,针对从用户模式的代码传给内核的任何实参,Windows都会把它们从线程的用户模式栈中复制到线程的内核模式栈。复制后内核就可以验证实参的值。由于应用程序的代码无法访问内核模式栈,所以应用程序无法修改验证后的实参值。OS内核代码开始对复制的值进行处理。除此之外,内核还会调用它自己内部的方法,并利用内核模式栈传递它自己的实参、存储函数的局部变量和存储返回地址。在Windows32位上运行时,内核模式栈大小为12KB,在64位上运行时大小则为24KB。
  • DLL线程连接和线程分离通知:任何时候在进程中创建一个线程,都会调用那个进程中加载的所有DLL的DllMain方法,并传递一个DLL_THREAD_ATTACH标记。同理任何时刻终止一个线程都会调用进程中所有DLL的DllMain方法,并传递一个DLL_THREAD_DETACH标记。有的DLL需要利用这些通知,为进程中创建/销毁的每个线程执行一些特殊的初始化或者清理操作。
线程上下文切换

在任何给定时刻,Windows只将一个线程分配给一个CPU,线程允许运行一个“时间片”,一旦“时间片”到期,Windows就将上下文切换到另一个线程,每次上下文切换Windows都要执行以下操作:

  • 将CPU寄存器中的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中
  • 从现有线程集合中选出一个线程供调度,如果这个线程属于另一个进程,Windows在开始执行任何代码或使用任何数据之前,还必须切换CPU的虚拟地址空间
  • 将所有上下文结构中的值加载到CPU寄存器中

上下文切换完成后,CPU执行所选的线程,直到它的“时间片”到期。然后会进行另一次上下文切换(大约每30msWindows会执行一次上下文切换)。上下文切换属于净开销,不具有任何性能上的收益。

如果应用程序的线程进入死循环,Windows将会定期抢占它,将一个不同的线程分配给一个实际的CPU,让新线程运行一会。假设新线程是“任务管理器”的线程,用户可以利用“任务管理器”终止包含死循环线程的进程。系统中的其它进程并不受影响扔可以继续运行,且不会丢失数据,也不必重启计算机,让用户拥有更好的体验。

Windows上下文切换到另一个线程时,会发生一定的性能损失。但是CPU现在是要执行一个不同的线程,而之前的线程的代码和数据还在CPU的高速缓存中,这让CPU不必经常访问RAM(访问RAM的速度比CPU高速缓存慢得多)。当Windows上下文切换到新线程时,新线程有可能要执行不同的代码并访问不同的数据,这些代码和数据不在CPU的高速缓存中,因此CPU必须访问RAM来填充它的高速缓存,以恢复高速运行的状态。但是30ms之后又会发生一次新的上下文切换。

*当一个时间片结束,再次调用同一个线程(而不是新的线程)时不会发生上下文切换,而会让线程继续运行,改善性能

垃圾回收性能与线程数量

执行垃圾回收时CLR会挂起所有线程,遍历它们的栈来查找根以便对堆中的对象进行标记,再次遍历栈,由于有的对象在压缩期间可能发生了移动, 需要更改新的根,再恢复所有线程。所以减少线程数量,可以提高垃圾回收的性能。

调试体验与线程数量

每次使用调试器并遇到一个断点,Windows都会挂起正在调试的应用程序中的所有线程,并在单步执行或者运行应用程序时恢复所有线程,因此线程越多,调试体验越差。

什么时候应该创建一个线程而不是使用线程池线程?
  • 线程需要以非普通线程优先级运行(所有线程池线程都以普通优先级运行)
  • 需要线程表现为一个前台线程,防止应用程序在线程结束它的任务之前终止(线程池线程始终是后台线程,如果终止进程它们可能无法完成任务)
  • 需要长时间运行一个任务,线程池为了判断是否需要创建一个额外的线程采用的逻辑较为复杂,直接创建则可以避开此问题
  • 要启动一个线程,并可以调用Thread的Abort方法提前终止
创建线程
private static void SomeMethod(object parameter)
{
    //方法由一个专用线程执行
    Console.WriteLine(parameter);
    Thread.Sleep(1000);

    //方法返回后专用线程将终止
}

static void Main()
{
    Thread thread1 = new Thread(SomeMethod);
    thread1.Start("start thread");

    //模拟做其它事情
    Thread.Sleep(10000);

    //等待线程终止
    thread1.Join();
    Console.WriteLine("按Enter键退出");
    Console.ReadKey();
}

结果
image.png

构造Thread对象是一个轻量级的操作,因为并没有实际上的创建一个操作系统线程。只有调用了Thread的Start方法才实际创建操作系统线程并让它开始执行回调方法。调用Thread对象的Join方法会使调用线程阻塞当前执行的任何代码,直到创建的线程(thread1)线程终止或销毁。

使用线程有什么好处?
  • 隔离代码,提高应用程序的可靠性
  • 简化代码
  • 实现并发执行
线程调度和优先级

Windows属于抢占式多线程操作系统,所谓的抢占式是指线程可以在任何时间停止(被抢占),并调度另一个线程。

线程的优先级从低到高为0-31,首先检查优先级为31的线程,并以轮流的方式调度它们。如果线程可以调度,就把它分配给一个CPU,在这个线程的时间片结束时,系统会检查是够存在另一个优先级为31的线程可以运行,如果是就将该线程分配给一个CPU。

只要存在优先级31的线程,系统就永远不会将优先级0-31的线程分配给CPU,出现较高优先级线程总是占用CPU的时间,导致较低优先级的线程始终无法运行的现象,该现象称为饥饿(在多处理器的机器上出现饥饿的情况很小)。

较高优先级的线程总是会抢占较低优先级的线程,无论正在运行的属于何种较低优先级的线程。如:有一个优先级为1的线程正在运行,现在系统确定有一个优先级为5的线程已经准备好运行了,系统会立即挂起(暂停)优先级为1的线程,将CPU分配给优先级为5的线程,该线程拥有一个完整的时间片

系统启动时,会创建整个系统中唯一的一个优先级为0的线程,称之为零页线程。这个线程负责在没有其它进程需要执行的时候,将系统RAM的所有空闲页清零

*高优先级线程在其生命周期中的大多数时间都应处于等待状态,这样才不会影响系统的总体响应能力

前台线程和后台线程

CLR将每个线程要么视为前台线程,要么视为后台线程。一个进程中的所有前台线程停止运行时,CLR将强制终止仍然在运行的所有后台线程。这些后台线程将直接终止且不会抛出异常。

前台线程和后台线程之间的差异

private static void SomeMethod()
{
    Thread.Sleep(10000);

    //只有被前台线程执行时才会显示出来
    Console.WriteLine("Something Done");
}

static void Main()
{
    //创建一个新的线程,默认为前台线程
    Thread t = new Thread(SomeMethod);

    //改变前台线程为后台线程
    t.IsBackground = true;

    //启动线程
    t.Start();

    //如果t是前台线程,应用程序10s后终止,如果是后台线程,应用程序立即终止
    Console.WriteLine("回到主线程");
}

结果:应用窗口一闪而过,且SomeMethod方法中的输出内容也没有显示

总结:
在一个线程的生存期中,任何时候都可以从前后台线程互相切换。应用程序的主线程和通过构造Thread对象显示创建的任何线程默认都是前台线程。而线程池线程默认都是后台线程。我们要尽量避免使用前台线程,而使用CLR的线程池,线程池会自动管理线程的创建以及销毁,同时线程池创建的线程可以为各种任务重复使用,所以应用程序通常只需要几个线程便可以完成全部工作。

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 1月28日

序列化和反序列化

什么是序列化和反序列化?

序列化和反序列化是对象和字节流之间的相互转换,将一个对象或者对象图转换成一个字节流的过程就是序列化;反之将一个字节流转换回对象图的过程便是反序列化。

序列化和反序列化的作用(包括但不限于)
  • 将对象克隆并作为备份
  • 将对象通过网络传输给另一台机器
  • 加密和压缩数据
简单的序列化与反序列化示例
private static MemoryStream SerializeToMemory(Object objectGraph)
{
    //构造一个流容纳序列化对象
    var stream = new MemoryStream();

    //构造序列化格式化器
    var formatter = new BinaryFormatter();

    //告诉格式化器将对象序列化到一个流中
    formatter.Serialize(stream, objectGraph);

    //返回序列化好的对象流
    return stream;
}

private static Object DeserializeFromMemory(Stream stream)
{
    //构造序列化格式化器
    var formatter = new BinaryFormatter();

    //告诉格式化器从流中反序列化对象
    return formatter.Deserialize(stream);
}

static void Main()
{
    var objectGraph = new List<string> { "Cat", "Dog", "Fish" };
    var stream = SerializeToMemory(objectGraph);

    //重置
    stream.Position = 0;
    objectGraph = null;

    //反序列化
    objectGraph = (List<string>)DeserializeFromMemory(stream);
    foreach (var item in objectGraph)
    {
        Console.WriteLine("item:{0}", item);
    }

    Console.ReadKey();
}

运行结果
image.png

格式化器知道如何序列化和反序列化一个对象,想要序列化、反序列化一个对象,只需要调用格式化器的Serialize或Deserialize方法。

**Serialize:**格式化器参考对每个对象的类型进行描述的元数据,从而了解如何序列化完成的对象。序列化时Serialize方法利用反射来查看每个对象的类型中都有哪些实例字段,在这些字段中任何一个引用了其它类型的字段,格式化器就知道也要对这些被引用了的字段进行序列化。

同时格式化器的算法也非常智能,能确保对象图中的每一个对象只序列化一次,如果对象图中的两个对象相互引用,在每个对象之序列化一次的前提下,便能够避免进入无限循环。

**Deserialize:**该方法会检查流的内容,构造流中的所有对象的实例,并初始化所有这些对象中的字段,使它们具有与当初序列化时相同的值。

序列化一个对象时,类型的全名和类型的定义程序集的名称也会被写入流。默认情况下BinaryFormatter会输出程序集的完整标识(无扩展名的文件名、版本号、语言文化和公钥信息)。反序列化一个对象时,格式化器首先获取程序集标识信息,并通过Assembly的Load方法,确保程序集加载到AppDomain中。程序集加载好之后,格式化器在程序集中查找与要反序列化的对象匹配的一个类型。如果程序集不包含要查找的类型,则抛出异常,并终止序列化。反之就创建类型的一个实例,并用流中包含的值对其字段进行初始化。如果类型中的字段与流中读取的字段名不完全匹配,抛出SerializationException异常,并终止序列化。

序列化类型

类型默认是无法序列化的

class Poiont
{
    public int x;
    public int y;
}

static void Main()
{
    var poiont = new Poiont() { x = 100, y = 100 };
    using(var stream = new MemoryStream())
    {
        new BinaryFormatter().Serialize(stream, poiont); //异常
    }
}

image.png
序列化一个对象图时,格式化器会检查每个对象的类型都是可序列化的。如果对象图中的任意一个对象不是可序列化的,格式化器的Serialize方法都会抛出SerializationException异常

使类型可序列化,让上述代码不再抛出异常

[Serializable]
class Poiont
{
    public int x;
    public int y;
}

序列化一个对象图时,可能存在有些对象可以被序列化,有些对象不能序列化的情况。出于性能格式化器不会在序列化前检查对象图中的所有对象都可序列化。所以在序列化对象图过程中抛出SerializationException异常,完全可能已经有有一部分对象已经序列化到了流中。

SerializableAttribute这个定制attribute的两个特征:
  • 只能应用于引用类型(class)、值类型(struct)、枚举类型和委托类型,其中枚举类型和委托类型总是可序列化的,不必显示应用SerializableAttribute
  • 不会被派生类继承
[Serializable]
class super { } //可序列化
class child : super { } //不可序列化
控制序列化和反序列化

SerializableAttribute应用于类型时,所有实例字段(private,protected,public等)都会被序列化。但是类型可能定义了一些不必序列化的实例字段(如序列化后信息无效或者通过简单的计算便能获得),如下代码展示了如何控制序列化和反序列化

[Serializable]
class Rectangle
{
    private float m_Width;
    public float Width
    {
        get { return this.m_Width; }
    }

    private float m_Height;
    public float Height
    {
        get { return this.m_Height; }
    }

    [NonSerialized]
    private float m_Area;
    public float Area
    {
        get { return this.m_Area; }
    }

    public Rectangle(float w, float h)
    {
        this.m_Width = w;
        this.m_Height = h;
        this.m_Area = w * h;
    }
}

上述代码中Rectangle的对象可以序列化,且格式化器只会序列化对象的m_Width和m_Height字段的值,由于m_Area字段使用了NonSerializedAttribute,所以m_Area字段的值不会被序列化。

static void Main()
{
    using(var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();
        formatter.Serialize(stream, new Rectangle(10, 5));

        stream.Position = 0;
        var rectangle = (Rectangle)formatter.Deserialize(stream);
        Console.WriteLine("w = {0}, h = {1}, area = {2}", rectangle.Width, rectangle.Height, rectangle.Area);
    }
}

结果
image.png

运行结果证明m_Area字段确实不会被序列化,因为反序列化的时候Area的值被初始化化为0而非50,但这有时候并不是所期望的,以下代码展示了如何修正该问题:

[Serializable]
class Rectangle
{
    //...
    //在原Rectangle类上扩展MyOnDeserialized方法
    [OnDeserialized]
    private void MyOnDeserialized(StreamingContext context)
    {
        this.m_Area = this.m_Width * this.m_Height;
    }
}

再次执行以下代码段:

static void Main()
{
    using(var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();
        formatter.Serialize(stream, new Rectangle(10, 5));

        stream.Position = 0;
        var rectangle = (Rectangle)formatter.Deserialize(stream);
        Console.WriteLine("w = {0}, h = {1}, area = {2}", rectangle.Width, rectangle.Height, rectangle.Area);
    }
}

结果
image.png

修改后的代码仅仅只是添加了一个使用OnDeserializedAttribute标记的MyOnDeserialized方法,每次反序列化类型的一个实例时,格式化器都会检查类型中是否定义了一个应用了OnDeserializedAttribute的方法。如果是就会调用这个方法,调用这个方法的时候所有可序列化的字段都会被正确的赋值。在该方法中,可以访问这些字段来进行一些额外的工作,从而保证对象的完全反序列化。

除了OnDeserializedAttribute,命名空间System.Runtime.Serialization中还定义了OnSerializingAttribute、OnSerializedAttribute、OnDeserializingAttribute。序列化一组对象时,格式化器首先调用对象的标记了OnSerializingAttribute的所有方法。接着序列化对象的所有字段。最后调用对象的标记了OnSerializedAttribute的所有方法。同理在反序列化一组对象时,格式化器先调用对象的标记了OnDeserializingAttribute的所有方法,然后反序列化对象的所有字段,最后调用对象的标记了OnDeserializedAttribute的所有方法。

***注:**在反序列化期间,格式化器发现一个类型中包含应用了OnDeserializedAttribute的方法时,格式化器会将这个对象的引用添加到一个内部列表中。等所有对象反序列化之后,格式化器以相反的方向遍历这个列表(相反顺序调用的原因是内层对象先于外层对象结束反序列化),调用每个对象的OnDeserializedAttribute标记方法,调用这个方法后,所有可序列化的字段都会被正确设置。

FormatterServices如何序列化和反序列化类型实例?

序列化

  • 调用FormatterServices的GetSerializableMembers方法,该方法利用反射获取类型的public和private实例字段,方法返回MemberInfo数组,每个元素对应一个可序列化的实例字段
  • MemberInfo数组传给FormatterServices的GetObjectData方法,返回Object数组,每个元素都标识了被序列化的那个对象中的一个字段的值。Object数组和MemberInfo数组是并行的(Object数组的0元素便是MemberInfo数组0元素的值)
  • 格式化器将程序集标识和类型的完整名称写入流
  • 格式化器遍历两个数组中的元素,将每个成员的名称和值写入流

反序列化

  • 格式化器从流中读取程序集标识和完整类型名称,如果程序集当前没有加载到AppDomain中,就加载它。如果程序集不能被加载就抛出异常,程序集已加载格式化器将程序集标识信息和类型全名传给FormatterServices的GetTypeFromAssembly方法,方法返回一个Type对象,代表要反序列化的对象类型
  • 调用FormatterServices的GetUninitializedObject方法,该方法为一个新对象分配内存,不会调用对象的构造器,对象的所有字段都会被初始化为0或null
  • 调用FormatterServices的GetSerializableMembers方法构造并初始化一个MemberInfo数组,方法返回序列化好的现在需要反序列化的一组字段
  • 格式化器根据流中包含的数据创建并初始化一个Object数组
  • 对新分配的对象、MemberInfo数组以及Object数组(包含字段的值)的引用传给FormatterServices的PopulateObjectMembers方法,这个方法会遍历数组,将每个字段初始化成对应的值,到此反序列化结束

示例代码

static void Main()
{
    var assembly = Assembly.Load("MySerialize");
    var type = FormatterServices.GetTypeFromAssembly(assembly, "MySerialize.Rectangle");
    var obj = FormatterServices.GetUninitializedObject(type);
    var mf = FormatterServices.GetSerializableMembers(type);
    var data = FormatterServices.GetObjectData(new Rectangle(10, 20), mf);
    var rect = (Rectangle)FormatterServices.PopulateObjectMembers(obj, mf, data);
    Console.WriteLine("w = {0}, h = {1}", rect.Width, rect.Height);
    Console.ReadKey();
}

运行结果
image.png

ISerializable控制序列化和反序列化

由于格式化器内部会使用反射,而反射的速度是比较慢的,所以会增加序列化和反序列化所花的时间。可以通过让类型实现ISerializable接口避免使用反射也可以序列化和反序列化对象。值得注意的是类型一旦实现了ISerializable接口,它的派生类也必须实现它,并且保证派生类调用了基类的GetObjectData方法。

格式化器在序列化对象图检查每个对象时,如果发现类型实现了ISerializable接口,便会忽略所有定制attribute,改为构造一个新的SerializationInfo对象,这个对象包含了实际要为对象序列化的值的集合。

构造并初始化好SerializationInfo对象后,格式化器调用对象的GetObjectData方法,并传递给方法SerializationInfo对象的引用。GetObjectData方法负责决定需要哪些信息来序列化对象,并将这些信息添加到SerializationInfo对象中,GetObjectData调用SerializationInfo类型提供的AddValue方法指定需要序列化的信息,对每个想要序列化的数据都要调用一次AddValue。

示例代码

[Serializable]
class Square : ISerializable, IDeserializationCallback
{
    private int m_Area;
    public int Area
    {
        get { return this.m_Area; }
    }

    private int m_Length;
    public int Length
    {
        get { return this.m_Length; }
    }

    private string m_Name;
    public string Name
    {
        get { return this.m_Name; }
    }

    //只用于反序列化
    private SerializationInfo m_SiInfo;

    public Square(string name, int len)
    {
        this.m_Length = len;
        this.m_Name = name;
    }

    //控制反序列化的特殊构造器
    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
    protected Square(SerializationInfo info, StreamingContext context)
    {
        //反序列化期间为OnDeserialization保存SerializationInfo
        //在这里不能保证对象已经完全被序列化
        this.m_SiInfo = info;
    }

    //控制序列化的方法
    [SecurityCritical]
    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Length", this.m_Length);
        info.AddValue("Name", this.m_Name);
    }

    //所有字段都被反序列化好之后调用该方法
    public virtual void OnDeserialization(object sender)
    {
        if (this.m_SiInfo == null)
            return;

        this.m_Length = this.m_SiInfo.GetInt32("Length");
        this.m_Name = this.m_SiInfo.GetString("Name");
        this.m_Area = (int)Math.Pow(this.m_Length, 2);
    }
}

static void Main()
{
    using (var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();
        formatter.Serialize(stream, new Square("正方形", 10));

        stream.Position = 0;
        var square = (Square)formatter.Deserialize(stream);
        Console.WriteLine("边长:{0}的{1}的面积是:{2}", square.Length, square.Name, square.Area);
    }
    Console.ReadKey();
}

运行结果
image.png

在反序列化的时候,格式化器从流中提取对象时,会为新对象分配新的内存(通过GetUninitializedObject方法),新对象的所有字段都设置为0或null。然后格式化器检查类型是否实现了ISerializable接口,如果是格式化器就尝试调用一个参数和GetObjectData方法一致的特殊构造器。

构造器获取一个SerializationInfo对象的引用,这个对象包含了对象序列化时添加的所有值,反序列化对象的字段时,可以调用和对象序列化时传给AddValue方法的值的类型匹配的一个Get方法,方法的返回值可用于初始化新对象的各个字段。

基类没有实现ISerializable接口,如何定义一个实现它的类型?

如果基类同样实现了ISerializable接口,那么只要调用基类的GetObjectData方法便能完成序列化。一旦基类没有实现GetObjectData接口,这种情况下派生类必须手动序列化基类的值。弊端:如果基类的值是private字段,那么根本就无法实现。

示例代码

[Serializable]
class Base
{
    protected string m_Name = "DoubleJ";

    public Base() { }
}

[Serializable]
class Child : Base, ISerializable
{
    private DateTime m_DateTime = DateTime.Now;

    public Child() { }

    protected Child(SerializationInfo info, StreamingContext context)
    {
        //获取类和基类可序列化成员集合
        var baseType = this.GetType().BaseType;
        var mi = FormatterServices.GetSerializableMembers(baseType, context);

        //反序列化基类字段
        for (int i = 0; i < mi.Length; i++)
        {
            var fi = (FieldInfo)mi[i];
            fi.SetValue(this, info.GetValue(baseType.FullName + "." + fi.Name, fi.FieldType));
        }

        this.m_DateTime = info.GetDateTime("Date");
    }

    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Date", this.m_DateTime);

        //获取类和基类可序列化成员集合
        var baseType = this.GetType().BaseType;
        var mi = FormatterServices.GetSerializableMembers(baseType, context);

        //序列化基类字段到info对象
        for (int i = 0; i < mi.Length; i++)
            info.AddValue(baseType.FullName + "." + mi[i].Name, ((FieldInfo)mi[i]).GetValue(this));
    }

    public override string ToString()
    {
        return string.Format("Name = {0}, Date = {1}", this.m_Name, this.m_DateTime);
    }
}

static void Main()
{
    using (var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();
        formatter.Serialize(stream, new Child());

        stream.Position = 0;
        var o = (Child)formatter.Deserialize(stream);
        Console.WriteLine(o.ToString());
    }
    Console.ReadKey();
}

运行结果
image.png

序列化代理
[Serializable]
class Version1Type
{
    public Int32 x;
}

//定义代理类
class MySerializationSurrogate : ISerializationSurrogate
{
    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
    {
        info.AddValue("x", ((Version1Type)obj).x);
    }

    public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
    {
        ((Version1Type)obj).x = info.GetInt32("x");
        return obj;
    }
}

static void Main()
{
    using (var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();

        //构造代理选择器对象
        var ss = new SurrogateSelector();

        //告诉代理选择器为Version1Type对象使用我们的代理
        ss.AddSurrogate(typeof(Version1Type), formatter.Context, new MySerializationSurrogate());

        //告诉格式化器使用代理选择器
        formatter.SurrogateSelector = ss;

        //序列化
        var beforeSerializeData = new Version1Type { x = 6 };
        formatter.Serialize(stream, beforeSerializeData);

        //反序列化
        stream.Position = 0;
        var afterSerializeData = (Version1Type)formatter.Deserialize(stream);

        Console.WriteLine("beforeSerializeX = {0}", beforeSerializeData.x);
        Console.WriteLine("afterSerializeX = {0}", afterSerializeData.x);
    }
}

运行结果
image.png

使用代理类型后,调用格式化器的Serialize方法时,会在SurrogateSelector维护的哈希表中查找要序列化的每个对象的类型,如果发现匹配类型就调用ISerializationSurrogate对象的GetObjectData方法获取应该写入流的信息。

调用格式化器的Deserialize方法时,会在SurrogateSelector中查找要反序列化的对象类型,如果发现匹配类型就调用ISerializationSurrogate对象的SetObjectData方法来设置反序列化的对象中的字段。

SurrogateSelector对象在内部维护了一个私有哈希表,调用AddSurrogate方法时,Type和StreamingContext构成哈希表的Key,ISerializationSurrogate对象便是哈希表的Value。如果要添加的Key已经存在,则会抛出ArgumentException。

如何将对象反序列化成另一个类型?

利用SerializationBinder类可以很方便地将一个对象反序列化成一个不同的类型,首先要定义好自己的类型如:MySerializationBinder类(名字任意),接着在构造好格式化器后,设置格式化器的Binder属性让它的值等于MySerializationBinder类型实例。在反序列化期间,格式化器发现设置了绑定器,对象在反序列化时格式化器都会调用绑定器的BindToType方法,并向方法传递程序集名称以及格式化器想要反序列化的类型。在内部可实现自己的逻辑返回实际想要反序列化的类型。

示例代码

[Serializable]
class Version1Type
{
    public int x;
}

[Serializable]
class Version2Type : ISerializable
{
    public int x;
    public string name;

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
    void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("x", x);
        info.AddValue("name", name);
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
    private Version2Type(SerializationInfo info, StreamingContext context)
    {
        x = info.GetInt32("x");
        try
        {
            name = info.GetString("name");
        }
        catch (SerializationException)
        {
            name = "default value";
        }
    }
}

class MySerializationBinder : SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        var assemVer1 = Assembly.GetExecutingAssembly().FullName;
        var typeVer1 = "MySerialize.Version1Type";
        if (assemblyName == assemVer1 && typeName == typeVer1)
            typeName = "MySerialize.Version2Type";

        //typeToDeserialize
        return Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));
    }
}

static void Main()
{
    using (var stream = new MemoryStream())
    {
        var formatter = new BinaryFormatter();

        //序列化
        Console.WriteLine("序列化的对象类型: " + typeof(Version1Type));
        formatter.Serialize(stream, new Version1Type() { x = 10 });
        formatter.Binder = new MySerializationBinder();

        stream.Position = 0;
        var type2 = (Version2Type)formatter.Deserialize(stream);

        Console.WriteLine("反序列化的对象类型: " + type2.GetType());
        Console.WriteLine("x = {0}, name = {1}", type2.x, type2.name);
    }
    Console.ReadKey();
}

运行结果
image.png

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 1月18日

反射

元数据

元数据是用一系列表来存储的,生成一个程序集或模块时,编译器会创建一个类型定义表、一个字段定义表、一个方法定义表以及其它表。

反射

程序运行的时候解析这些元数据表以获取信息,该行为便是反射。反射允许在运行时发现并使用编译时还不了解的类型及其成员。

反射的性能
  • 反射会造成编译时无法保证类型安全性。反射需要重度依赖字符串,所以会丧失编译时的类型安全。例如执行Type.GetType("A"),在一个程序集中查找类型名为“A”的类型,但程序集实际包含的类型可能是“AA”,代码会通过编译,但运行时会出错。
  • 反射速度慢,使用反射时,类型及其成员的名称在编译时未知,要使用字符串名称标识每个类型及其成员,以便在运行时发现它们。也就是说在扫描程序集的元数据时,反射要不断的执行字符串的搜索,而字符串的搜索执行的是不区分大小写的比较,这会进一步影响速度。
反射调用一个成员时也会对性能产生影响

反射调用方法时,首先必须将实参打包成一个数组,在内部反射必须将这些实参解包到线程栈上。此外在调用方法前,CLR必须检查实参具有正确的数据类型,最后CLR还需确保调用者有正确的安全权限来访问被调用的成员。

*综上所述,最好避免使用反射技术来访问字段或者调用方法。

获取Type对象的几种方式
  • Type的静态方法GetType:接收一个string参数,必须指定类型的全名(包括命名空间),如果调用程序集没有定义指定的类型,就查找MSCorLib.dll定义的类型,如果还是没找到就返回null
  • Type的静态方法ReflectionOnlyGetType:行为与GetType相似,只是类型会加载到一个“仅反射”上下文中,不能执行
构造类型的实例
  • Activator的静态方法CreateInstance:调用该方法可以传递一个Type对象引用,也可以传递标识了想要创建的类型的一个String。其中直接获取一个类型对象的几个重载版本相对简单,为类型的构造器传递一组实参,方法返回的是对新对象的一个引用。而使用字符串来指定所需类型的几个重载版本要稍微复杂一些,首先必须指定另一个字符串来标识定义了类型的那个程序集。其次这些版本返回的不是对新对象的引用,而是一个System.Runtime.Remoting.ObjectHandle对象。ObjectHandle类型允许将一个AppDomain中创建的对象传至其它AppDomain,期间不强迫对象具体化,要具体化该对象可以调用ObjectHandle对象的Unwrap方法。在一个AppDomain中调用这个方法时,它会将定义了要具体化的类型的程序集加载到这个AppDomain中。如果对象按引用封送就创建代理类型和对象。如果按值封送,对象的副本会被反序列化。
  • Activator的静态方法CreateInstanceFrom:行为与CreateInstance方法相似,不同的是必须通过字符串来指定类型及其程序集。程序集要使用Assembly的LoadFrom(而非Load)方法加载到调用的AppDomain中。由于没有接收Type参数的版本,所以返回的都是ObjectHandle对象的引用,必须调用ObjectHandle的Unwrap方法进行具体化
构造泛型类型的实例
static void Main()
{
    //获取泛型类型的类型对象的一个引用
    Type temp = typeof(List<>);

    //使用int类型封闭泛型类型
    Type closedType = temp.MakeGenericType(typeof(int));

    //构造封闭类型的实例
    object o = Activator.CreateInstance(closedType);

    Console.WriteLine(o.GetType());
}

运行结果
image.png

发现类型成员

字段、构造器、方法、属性、事件和嵌套类型都可以被定义为类型的成员。FCL包含一个System.Reflection.MemberInfo的类型,封装了一组所有类型都通用的属性。层次结构图如下:
image.png

查询一个类型的成员并显示与之相关的一些信息
static void Main()
{
    Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
    //遍历AppDomain中加载的所有程序集
    foreach (var item in assemblies)
    {
        Console.WriteLine("Assembly: {0}", item);

        //查找程序集中的类型
        foreach (var t in item.GetExportedTypes())
        {
            Console.WriteLine("Type: {0}", t);

            //发现类型成员
            const BindingFlags bf = BindingFlags.DeclaredOnly |
                BindingFlags.NonPublic | BindingFlags.Public |
                BindingFlags.Instance | BindingFlags.Static;

            foreach (var mi in t.GetMembers(bf))
            {
                var typeName = string.Empty;
                if (mi is Type)
                    typeName = "Type";
                else if (mi is FieldInfo)
                    typeName = "FieldInfo";
                else if (mi is MethodInfo)
                    typeName = "MethodInfo";
                else if (mi is ConstructorInfo)
                    typeName = "ConstructorInfo";
                else if (mi is PropertyInfo)
                    typeName = "PropertyInfo";
                else if (mi is EventInfo)
                    typeName = "EventInfo";

                Console.WriteLine("mi typeName: {0}", typeName);
            }
        }
    }
}
BindingFlags枚举(筛选返回的成员类型)
  • Default:指定未定义任何绑定标志
  • IgnoreCase:返回与指定字符串匹配的成员(忽略大小写)
  • DeclaredOnly:只返回被反射的那个类型的成员,忽略继承的成员
  • Instance:返回实例成员
  • Static:返回静态成员
  • Public:返回公共成员
  • NonPublic:返回非公共成员
  • FlattenHierarchy:返回基类型定义的公共成员和受保护静态成员

遍历反射对象模型图
image.png

基于一个AppDomain可发现加载到其中的所有程序集。基于一个程序集可发现构成它的所有模块。基于一个程序集或模块可发现它定义的所有类型。基于一个类型可发现它的嵌套类型、字段、构造器、方法、属性和事件

发现类型的接口
interface ITest1 : IDisposable
{
    void Method1();
    void Method2();
}

interface ITest2
{
    void Method1();
}

class MyClass : ITest1, ITest2, IDisposable
{
    //ITest1
    void ITest1.Method1() { }

    public void Method2() { }

    //ITest2
    void ITest2.Method1() { }

    //IDisposable
    public void Dispose() { }

    //非接口方法
    public void Method1() { }
}

class Program
{
    static void Main()
    {
        //查找MyClass实现的接口
        Type t = typeof(MyClass);
        Type[] interfaces = t.FindInterfaces(TypeFilter, typeof(Program).Assembly);

        //显示每个接口的信息
        foreach (var item in interfaces)
        {
            Console.WriteLine("接口: {0}", item);

            //获取接口映射
            InterfaceMapping map = t.GetInterfaceMap(item);

            for (int i = 0; i < map.InterfaceMethods.Length; i++)
            {
                Console.WriteLine("方法 {0} 在 {1} 中实现", map.InterfaceMethods[i], map.TargetMethods[i]);
            }
        }

        Console.ReadKey();
    }

    private static bool TypeFilter(Type t, object filterCriteria)
    {
        //如果接口是由filterCriteria标识的程序集中定义的就返回true
        return t.Assembly == (Assembly)filterCriteria;
    }
}

结果
image.png

调用类型的成员

Type类提供了一个InvokeMember方法,可以通过这个方法调用一个类型的成员。在调用这个方法时,会在类型的成员中搜索一个匹配的成员,如果没有找到则会抛出一个异常。反之则会调用成员,该成员返回什么InvokeMember方法也会返回一个同样的信息数据。

InvokeMember使用的BindingFlags
image.png

一次绑定多次调用

使用InvokeMember方法的弊端在于每次调用该方法,都必须先绑定到一个特定的成员然后才能调用,这大大地增加了耗时。可以直接调用Type的某个方法来绑定成员(GetFields,GetMethods等),可以返回对一个对象的引用,通过对象引用直接访问特定成员。

示例代码

class SomeType
{
    private int m_SomeValue;
    public int SomeValue
    {
        get
        {
            return this.m_SomeValue;
        }

        set
        {
            this.m_SomeValue = value;
        }
    }

    public string SomeMethod()
    {
        Console.WriteLine("Run SomeMethod");
        return "SomeMethod Rusult";
    }
}

static void Main()
{
    var bf = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    Type t = typeof(SomeType);
    //构造Type实例
    object o = t.InvokeMember(null, bf | BindingFlags.CreateInstance, null, null, null);
    Console.WriteLine("o type : {0}", o.GetType().ToString());

    //读写字段
    t.InvokeMember("m_SomeValue", bf | BindingFlags.SetField, null, o, new object[] { 1 });
    int v = (int)t.InvokeMember("m_SomeValue", bf | BindingFlags.GetField, null, o, null);
    Console.WriteLine("m_SomeValue = {0}", v);

    //调用一个方法
    string methodRes = (string)t.InvokeMember("SomeMethod", bf | BindingFlags.InvokeMethod, null, o, null);
    Console.WriteLine("methodRes = {0}", methodRes);

    Console.WriteLine("\n---------分割线---------\n");

    //构造实例
    ConstructorInfo ctor = t.GetConstructor(new Type[] { });
    o = ctor.Invoke(null);

    //读写字段
    FieldInfo fi = o.GetType().GetField("m_SomeValue", bf);
    fi.SetValue(o, 2);
    Console.WriteLine("m_SomeValue = {0}", fi.GetValue(o));

    //调用方法
    MethodInfo mi = o.GetType().GetMethod("SomeMethod", bf);
    methodRes = (string)mi.Invoke(o, null);
    Console.WriteLine("methodRes = {0}", methodRes);

    Console.ReadKey();
}

结果
image.png

使用句柄减少进程中内存的消耗

由于Type和MemberInfo的派生对象需要大量内存,因此如果应用程序容纳了太多这样的对象,但只是偶尔调用一下,内存消耗将急剧增长,对性能产生负面影响。

解决方法:如果需要大量缓存Type和MemberInfo的派生对象,我们可以使用允许时句柄来替代对象,从而减小工作集。FCL定义了三个运行时句柄类型,分别是:RuntimeTypeHandle,RuntimeFieldHandle,RuntimeMethodHandle,三个类型均属于值类型,只包含一个IntPtr字段,这个字段是一个句柄,引用了AppDomain的Loader堆中的一个类型、字段或方法。

转化方法:

  • Type转RuntimeTypeHandle:调用Type的静态方法GetTypeHandle
  • RuntimeTypeHandle转Type:调用Type的静态方法GetTypeFromHandle
  • FieldInfo转RuntimeFieldHandle:查询FieldInfo实例只读字段FieldHandle
  • RuntimeFieldHandle转FieldInfo:调用FieldInfo的静态方法GetFieldFromHandle
  • MethodInfo转RuntimeMethodHandle:查询MethodInfo实例只读字段MethodHandle
  • RuntimeMethodHandle转MethodInfo:调用MethodInfo的静态方法GetMethodFromHandle

示例代码

private static void Show(string s)
{
    Console.WriteLine("堆大小 : {0,12:##,###,###} - {1}", GC.GetTotalMemory(true), s);
}

static void Main()
{
    var bf = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

    Show("任何反射操作之前的堆的大小");

    //为MSCorlid.dll中所有方法构建MethodInfo对象缓存
    List<MethodBase> methodBases = new List<MethodBase>();
    foreach (var item in typeof(Object).Assembly.GetExportedTypes())
    {
        if (item.IsGenericTypeDefinition)
            continue;

        MethodBase[] mb = item.GetMethods(bf);
        methodBases.AddRange(mb);
    }
    Console.WriteLine("方法个数 : {0}", methodBases.Count);
    Show("绑定所有方法后堆的大小");

    //为所有MethodInfo对象构建RuntimeMethodHabdle缓存
    List<RuntimeMethodHandle> runtimeMethodHandles = methodBases.ConvertAll<RuntimeMethodHandle>(mb => mb.MethodHandle);
    Show("构建RuntimeMethodHandle后堆的大小");

    //阻止缓存被过早垃圾回收
    GC.KeepAlive(methodBases);

    //垃圾回收
    methodBases = null;
    Show("垃圾回收methodBases后堆的大小");

    Console.ReadKey();
}

结果
image.png

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 1月14日

AppDomain

什么是AppDomain

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

AppDomain作用
  • 一个AppDomain中的代码创建的对象不能由另一个AppDomain中的代码直接访问。一个AppDomain中的代码创建一个对象后,该对象被该AppDomain拥有。这个对象的生存期不可能比创建该对象的代码所在的AppDomain生存期长。一个AppDomain中的代码要访问另一个AppDomain中的对象,必须使用“按引用封送”或者“按值封送”的语义。从而加强AppDomain之间的边界,使得两个不同的AppDomain之间不存在对象之间的直接引用。所以可以很容易的从一个进程中卸载AppDomain而不会影响到其它应用程序中正在运行的代码
  • AppDomain可以卸载,CLR不支持卸载AppDomain中的单个程序集,可以卸载AppDomain从而卸载包含在该AppDomain中的所有程序集
  • Appdomain可以单独保护,AppDomain在创建之后会应用一个权限集,权限集决定了向AppDomain中运行的程序集授予的最大权限。保证当宿主加载一些代码后,这些代码不会破坏宿主本身使用的一些重要数据结构
  • AppDomain可以单独实施配置,AppDomain在创建之后会关联一组配置设置,这些设置主要影响CLR在AppDomain中加载程序集的方式。如:搜索路径、版本绑定重定向、卷影复制以及加载器优化
Windows进程图

image.png

上图所示有两个APPdomain,分别为AppDomain#1(默认AppDomain)和AppDomain#2。其中AppDomain#1包含3个程序集:MyApp.exe,TypeLib.dll,System.dll。AppDomain#2包含2个程序集:Wintellect.dll和System.dll。

System.dll被加载到两个程序集中,假设两个AppDomain都使用了来自System.dll中的同一个类型,那么在两个AppDomain的Loader堆中都会为同一个类型分配一个类型对象,类型对象的内存不会由两个AppDomain共享。另外,一个AppDomain中的代码调用一个类型调用的方法时,方法的IL代码会进行JIT编译,生成的本地代码将与每一个AppDomain关联,方法的代码不由调用它的所有AppDomain共享。

虽然不共享类型对象的内存或者本地代码是一种浪费,但是AppDomain的全部目的是提供隔离性。CLR要求在卸载某个AppDomain并释放它的所有资源的同时,不会对其它AppDomain产生负面影响。

有些程序集本来就会被多个AppDomain使用,如MSCorLib.dll,该程序集包含了System.Object,System.Int32以及其它所有与.NET Framework密不可分的类型。CLR初始化时,该程序集会自动加载,而且所有AppDomain都共享该程序集中的类型。为了减少资源消耗,该程序集通过“AppDomain中立”的方式加载,CLR会为它们维护一个特殊的Loader堆,该Loader堆中的所有类型对象以及为这些类型定义的方法JIT编译生成的所有本地代码,都会被进程中的所有AppDomain共享。

*共享资源的代价:“AppDomain中立”的方式加载的所有程序集永远不能被卸载,为了回收它们占用的资源唯一的方法便是终止Windows进程,让Windows回收资源。

跨AppDomain访问对象
  • 按引用封送
//按引用封送
public class MarshalByRefType : MarshalByRefObject
{
    public MarshalByRefType()
    {
        Console.WriteLine("{0} 在 {1} 中执行", this.GetType().ToString(), Thread.GetDomain().FriendlyName);
    }

    public void SomeMethod()
    {
        Console.WriteLine("SomeMethod 在 {0} 中执行", Thread.GetDomain().FriendlyName);
    }

    public MarshalByValType MethodWithReturn()
    {
        Console.WriteLine("MethodWithReturn 在 {0} 中执行", Thread.GetDomain().FriendlyName);
        MarshalByValType t = new MarshalByValType();
        return t;
    }

    public NonMarshalableType MethodArgAndReturn(string callDomainName)
    {
        Console.WriteLine("AppDomain {0} 调用 AppDomain {1}", callDomainName, Thread.GetDomain().FriendlyName);
        NonMarshalableType t = new NonMarshalableType();
        return t;
    }
}
  • 按值封送
//按值封送
[Serializable]
public class MarshalByValType : Object
{
    private DateTime m_CreateTime = DateTime.Now;

    public MarshalByValType()
    {
        Console.WriteLine("{0} 在 {1} 中执行,创建于 {2}", this.GetType().ToString(), Thread.GetDomain().FriendlyName, this.m_CreateTime);
    }

    public override string ToString()
    {
        return this.m_CreateTime.ToLongDateString();
    }
}
  • 完全不能封送类型
//该实例无法跨AppDomain传送
public class NonMarshalableType : Object
{
    public NonMarshalableType()
    {
        Console.WriteLine("创建NonMarshalableType 在 {0} 中执行", Thread.GetDomain().FriendlyName);
    }
}

调用代码

 class Program
{
    static void Main(string[] args)
    {
        //获取AppDomain引用
        AppDomain appDomain = Thread.GetDomain();

        //获取AppDomain名称
        string appDomainName = appDomain.FriendlyName;
        Console.WriteLine("默认AppDomain FriendlyName = {0}", appDomainName);

        //获取AppDomain中包含Main方法的程序集
        string exeAssembly = Assembly.GetEntryAssembly().FullName;
        Console.WriteLine("Main assembly = {0}", exeAssembly);

        //定义局部变量引用AppDomain
        AppDomain appDomain1 = null;

        //按引用封送
        Console.WriteLine("{0} Demo 1", Environment.NewLine);

        //新建一个AppDomain
        appDomain1 = AppDomain.CreateDomain("MyAppDomain1", null, null);
        MarshalByRefType mbrt = null;

        //将程序集加载到新的AppDomain中,构造对象把它封送到新建的AppDomain(实际得到一个代理引用)
        mbrt = (MarshalByRefType)appDomain1.CreateInstanceAndUnwrap(exeAssembly, "MyAppDomain.MarshalByRefType");
        Console.WriteLine("Type = {0}", mbrt.GetType());

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

        //看起来像是在MarshalByRefType上调用一个方法,其实是在代理类型上调用方法
        //代理使线程转至拥有对象的那个AppDomain,并在真实的对象上调用这个方法
        mbrt.SomeMethod();

        //卸载创建的AppDomain
        AppDomain.Unload(appDomain1);

        //mbrt引用一个有效的代理对象,代理对象引用一个无效的AppDomain
        try
        {
            //在代理对象上调用一个方法,AppDomain无效抛出异常
            mbrt.SomeMethod();
            Console.WriteLine("调用成功");
        }
        catch (AppDomainUnloadedException)
        {
            Console.WriteLine("调用失败");
        }

        //按值封送
        Console.WriteLine("{0} Demo 2", Environment.NewLine);

        //新建一个AppDomain
        appDomain1 = AppDomain.CreateDomain("MyAppDomain2", null, null);

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

        //对象的方法返回对象的一个副本,对象按值传送
        MarshalByValType mbvt = mbrt.MethodWithReturn();

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

        //看起来像是在MarshalByRefType上调用一个方法,实际也是如此
        Console.WriteLine(mbvt.ToString());

        //卸载创建的AppDomain
        AppDomain.Unload(appDomain1);

        //mbvt引用有效的对象,卸载AppDomain没有影响
        try
        {
            //不会抛出异常
            Console.WriteLine(mbvt.ToString());
            Console.WriteLine("调用成功");
        }
        catch (AppDomainUnloadedException)
        {
            Console.WriteLine("调用失败");
        }

        //新建一个AppDomain
        appDomain1 = AppDomain.CreateDomain("MyAppDomain3", null, null);

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

        Console.WriteLine("{0} Demo 3", Environment.NewLine);

        //对象的方法返回一个不可封送的对象,抛出异常
        try
        {
            NonMarshalableType nmt = mbrt.MethodArgAndReturn(appDomainName);
        }
        catch (Exception e)
        {

            Console.WriteLine(e.Message);
        }

        Console.ReadKey();
    }
}

运行结果
image.png

代码分析:
首先获得一个AppDomain对象的引用,当前调用线程正在这个AppDomain中执行。由于多个AppDomain可以在一个Windows进程中,所以线程能执行一个AppDomain中的代码再执行另一个AppDomain中的代码。从CLR的角度看线程一次只能执AppDomain中的代码。

AppDomain创建之后可以赋予它一个友好名称用来标识AppDomain,CLR使用可执行文件的文件名来作为默认的AppDomain的友好名称。

按引用封送
调用CreateDomain告诉CLR在同一个进程中创建一个新的AppDomain,新的AppDomain有自己的Loader堆(目前是空的),因此还没有程序集被加载到Loader中。创建AppDomain时CLR不在这个AppDomain中创建任何线程,AppDomain中也没有代码运行,除非显示的让一个线程调用AppDomain中的代码。

为了在新的AppDomain中创建一个新类型的实例,首先必须将一个程序集加载到AppDomain中,然后构建该程序集中定义的一个类型的实例。CreateInstanceAndUnwrap做的便是这个事情,该方法接收两个参数,第一个参数表示AppDomain要加载的程序集,第二个参数表示想要构建实例对象的类型名称。在内部该方法将导致调用线程从当前AppDomain转至新的AppDomain,现在线程将指定的程序集加载到新的AppDomain中,并扫描程序集的类型定义元数据表,查找指定的类型(MyAppDomain.MarshalByRefType"),找到类型后创建该类型实例,线程返回默认的AppDomain,使得CreateInstanceAndUnwrap能返回对新的MarshalByRefType对象的引用。

由于CLR并不允许一个AppDomain中的变量引用另一个AppDomain中创建的对象,因此在CreateInstanceAndUnwrap方法返回对象的引用之前还需要执行一些额外的逻辑。

引用返回之前的额外工作
MarshalByRefType类型是从System.MarshalByRefObject派生的,这个类是一个非常特殊的基类,当CreateInstanceAndUnwrap发现自己封送的一个对象的类型派生自MarshalByRefObject时,CLR就会跨AppDomain边界按引用封送对象。

源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR会在目标AppDomain的Loader堆中定义一个代理类型,这个代理类型是用原始类型的元数据定义的。因此它看起来和原始类型完全一样:有完全一样的实例成员(属性、事件和方法)。但是实例字段不会成为代理类型的一部分。

在目标AppDomain中定义好代理类型后,CreateInstanceAndUnwrap方法会创建这个代理类型的一个实例,初始化它的字段来标识AppDomain和真实对象,然后将这个代理对象的引用返回目标AppDomain。调用RemotingServices.IsTransparentProxy证明返回的确实是一个代理对象。

接着引用程序使用代理来调用SomeMethod方法,由于mbrt引用一个代理对象,所以会调用由代理实现的SomeMethod方法。在代理的调用中,利用了代理对象中的信息字段,将调用线程从默认AppDomain切换至新的AppDomain。现在该线程的任何行为都在新AppDomain的安全策略和配置下执行。然后线程使用代理对象的GCHandle字段查找新AppDomain中的真是对象,并用真是对象调用真是的SomeMethod方法。

*一个AppDomain中的线程调用另一个AppDomain中的方法时,线程会在两个AppDomain中进行切换,这也意味着跨AppDomain边界的方法调用是同步的。任意时刻一个线程只能在一个AppDomain中

紧接着调用Unload方法强制CLR卸载指定的AppDomain,并强制执行一次垃圾回收,释放由卸载的AppDomain中的代码创建的所有对象。此时默认的AppDomain还引用着一个有效的代理对象,但是代理对象不再引用一个有效的AppDomain。此时再试图调用SomeMothed时,调用的是该方法在代理中的实现,代理发现真实对象的AppDomain已经卸载,所以抛出异常。

按值封送
代码与按引用封送类似,不同的是MarshalByValType不是从MarshalByRefObject派生的,所以CLR不能定义一个代理类型,并创建一个代理类型的实例。对象不能按引用封送,但是由于MarshalByValType标记了[Serializable],所以CreateInstanceAndUnwrap能够按值封送对象。

源AppDomain想向目标AppDomain发送或返回一个对象引用时,CLR将对象的实例字段序列化成一个字节数组。这个字节数组从源AppDomain复制到目标AppDomain,然后CLR在目标AppDomain中反序列化字节数组,这个操作会强制CLR将定义了被反序列化的类型的程序集加载到目标AppDomain中。接着CLR创建类型的一个实例,并利用字节数组中的值初始化对象的字段,使之与源对象中的值相同。然后CreateInstanceAndUnwrap返回对这个副本的引用。如此便实现了对象的跨AppDomain边界按值封送。

到此源AppDomain中的对象和目标AppDomain中的对象就有了独立生存期,它们的状态可以独立地更改。如果源AppDomain中没有根保持源对象地存活,源对象的内存会在下一次垃圾回收时被回收。

接下来程序使用真实对象调用ToString方法,由于mbrt引用一个真实的对象,所以会调用这个方法的真实实现,线程不会在AppDomain之间切换。

为了进一步证明不是代理对象,现在将AppDomain卸载,继续调用ToString方法,调用仍然成功。

不可封送类型
由于NonMarshalableType类型既没有派生自MarshalByRefObject也没有[Serializable]标记,所以不能按引用封送也不能按值封送,对象完全不能跨AppDomain边界进行封送。同时抛出一个SerializationException异常。

监视AppDomain

可以将AppDomain的静态属性MonitoringIsEnabled设置为true,从而监视AppDomain的资源消耗情况。

示例代码

class AppDomainMonitorDelta : IDisposable
{
    private AppDomain m_AppDomain;
    private TimeSpan m_ThisADCpu;
    private long m_ThisADMemoryInUse;
    private long m_ThisAdMemoryAllocated;

    static AppDomainMonitorDelta()
    {
        //打开AppDomain监视
        AppDomain.MonitoringIsEnabled = true;
    }

    public AppDomainMonitorDelta(AppDomain appDomain)
    {
        this.m_AppDomain = appDomain ?? AppDomain.CurrentDomain;
        this.m_ThisADCpu = this.m_AppDomain.MonitoringTotalProcessorTime;
        this.m_ThisADMemoryInUse = this.m_AppDomain.MonitoringSurvivedMemorySize;
        this.m_ThisAdMemoryAllocated = this.m_AppDomain.MonitoringTotalAllocatedMemorySize;
    }

    public void Dispose()
    {
        GC.Collect();
        Console.WriteLine(
            "FriendlyName={0}, CPU={1}ms",
            this.m_AppDomain.FriendlyName, (this.m_AppDomain.MonitoringTotalProcessorTime - this.m_ThisADCpu).TotalMilliseconds
        );
        Console.WriteLine(
            "Allocated {0:N0} bytes of which {1:N0} survived GCs",
            this.m_AppDomain.MonitoringTotalAllocatedMemorySize - this.m_ThisAdMemoryAllocated,
            this.m_AppDomain.MonitoringSurvivedMemorySize - this.m_ThisADMemoryInUse
        );
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (new AppDomainMonitorDelta(null))
        {
            //分配回收时会存活的约10M字节
            var list = new List<Object>();
            for (int i = 0; i < 1000; i++)
                list.Add(new Byte[10000]);

            //分配回收时不会存活的约20M字节
            for (int i = 0; i < 2000; i++)
                new Byte[10000].GetType();

            //保持CPU工作约5秒
            long stop = Environment.TickCount + 5000;
            while (Environment.TickCount < stop) ;
        }

        Console.ReadKey();
    }
}

输出结果
image.png

AppDomain类的4个只读属性

  • MonitoringSurvivedProcessMemorySize:Int64属性,返回由当前CLR实际控制的所有AppDomain正在使用的字节数,只保证在上一次垃圾回收时是准确的
  • MonitoringTotalAllocatedMemorySize:Int64属性,返回一个特定的AppDomain已分配的字节数,只保证在上一次垃圾回收时是准确的
  • MonitoringSurvivedMemorySize:Int64属性,返回一个特定的AppDomain当前正在使用的字节数,只保证在上一次垃圾回收时是准确的
  • MonitoringTotalProcessorTime:TimeSpan属性,返回一个特定的AppDomain的CPU占用率
AppDomain卸载

卸载AppDomain会导致CLR卸载AppDomain中的所有程序集,还会释放AppDomain的Loader堆。可以调用AppDomain的静态方法Unload卸载AppDomain。

卸载AppDomain时CLR执行的一系列操作

  • CLR挂起进程中执行过托管代码的所有线程
  • CLR检查所有线程栈,查看有哪些线程正在执行要卸载的那个AppDomain中的代码,或者哪些线程会在某个时刻返回至要卸载的那个AppDomain。在任何一个栈上,如果有准备卸载的AppDomain,CLR都会强迫对应的线程抛出一个ThreadAbortException异常并同时恢复线程的执行。这将导致线程展开,在展开的过程中执行遇到的所有finally块中的代码,以进行资源清理。如果没有代码捕捉ThreadAbortException异常,它会成为一个未处理的异常,并且CLR会吞噬该异常。线程会终止,但进程会继续运行(这一点非常特殊,因为对于其它所有未处理的异常CLR都会终止进程)
  • 当上一步发现的所有线程都离开AppDomain后,CLR遍历堆,为引用了“由已卸载的AppDomain创建的对象”的每一个代理对象都设置一个标志。这些代理对象现在知道它们引用的真实对象已经不存在了,如果任何代码试图调用一个无效的代理对象上的方法,该方法会抛出AppDomainUnloadException
  • CLR强制垃圾回收,对现已卸载AppDomain创建的任何对象占用的内存进行回收。并调用这些对象的Finalize方法,彻底清理对象所占用的资源
  • CLR恢复所有剩余线程的执行,调用AppDomain.Unload方法的线程继续运行(AppDomain.Unload的调用是同步进行的)

*如果调用AppDomain.Unload方法的线程正好在要卸载的AppDomain中,CLR会创建一个新的线程来尝试卸载AppDomain。第一个线程被强制抛出ThreadAbortException并展开,新建的线程将等待AppDomain卸载,然后新线程终止。

FirstChance异常通知

给AppDomain的实例事件FirstChanceException添加委托可以在捕捉到异常的时候获得回调。

CLR异常处理:异常首次抛出时,CLR会调用已向抛出异常的那个AppDomain登记的FirstChanceException回调方法。然后CLR查找栈上在同一个AppDomain中的任何catch块,如果有一个catch块能处理异常,则异常处理完成,程序继续正常执行。如果AppDomain中没有一个catch块能处理异常,则CLR沿着栈向上调用AppDomain,再次抛出同一个异常对象。CLR会继续调用已向当前AppDomain登记的FirstChanceException回调方法,该过程会一直持续,知道抵达线程栈的顶部。如果异常还未被任何代码处理,CLR将终止整个进程。

*FirstChanceException只负责监视AppDomain抛出异常时获取一个通知,回调方法并不能处理异常

可执行应用程序执行过程

Windows通过一个托管EXE文件初始化一个进程时,会加载垫片。垫片会检查包含在EXE文件中的CLR头信息。头信息指明生成和测试应用程序时使用的CLR版本(垫片根据这个信息决定哪个版本的CLR加载到进程中),CLR加载并初始化好之后,它会检查程序集的CLR头,判断应用程序的入口是哪个(Main方法),CLR调用这个方法使应用程序真正启动并运行。
代码运行时会访问其它类型,引用另一个程序集的类型时CLR会定位所需的程序集,并把它加载到同一个AppDomain中。当应用程序的Main方法返回后,Windows进程终止并销毁默认的AppDomain和其它所有AppDomain。

*可调用System.Environment.Exit方法关闭Windows进程,该方法能保证所有对象的Finalize方法被执行

当托管代码出现错误时,CLR可以做什么?
  • 如果一个线程的执行时间过长,CLR可以终止线程并返回一个响应
  • CLR可以卸载AppDomain,从而卸载有问题的代码
  • CLR可以被禁用,阻止更多的托管代码在程序中运行
  • CLR可以退出Windows进程(先终止所有线程后卸载所有AppDomain)
宿主如何拿回它的线程?

宿主应用程序一般都要保持对自己线程的控制,以数据库服务为例:新请求抵达数据库,线程A获得该请求,后把该请求派发给线程B执行实际工作。假设线程B要执行的代码进入无限循环,这将导致数据库服务器派发的线程B一去不复返了,如此服务器是不是应该创建更多的线程,而这些线程本身也可能进入无限循环。

宿主可利用线程终止功能解决上述问题,线程终止工作方式如图:
image.png

  • 1、客户端向服务器发送一个请求
  • 2、服务器接到该请求并把它一个线程池来执行实际工作
  • 3、线程池线程获得该请求,开始执行可信代码
  • 4、可信代码进入try块,跨越AppDomain边界调用代码(包含不可信代码)
  • 5、宿主在接到客户端的请求时会记录一个时间,如果不可信代码在设定的时间期限内没有做出响应,宿主就会调用Thread的Abort方法终止线程,强制抛出ThreadAbortException
  • 6、线程开始展开,调用finally块进行清理工作。最终线程池线程穿越AppDomain返回。由于宿主代码是从一个try块中调用不可信代码的,所以宿主有一个catch块捕捉ThreadAbortException异常
  • 7、为了响应捕捉到的ThreadAbortException异常,宿主调用Thread的ResetAbort方法
  • 8、由于宿主的代码已经捕捉到了ThreadAbortException异常,因此宿主可以向客户端返回某种形式的错误,允许线程池线程返回线程池,供未来的新请求使用

ThreadAbortException是一个比较特殊的异常,即使代码捕捉了该异常,CLR也不允许将该异常吞噬,即在catch块的尾部CLR会重新抛出该异常。同时支持调用Thread的ResetAbort方法告诉CLR不需要在catch的尾部重新抛出ThreadAbortException异常。而调用ResetAbort方法时要求调用者被授予SecurityPermission权限并且ControlThread标志被设置为true,但是宿主在为不可信代码创建AppDomain时不会向其授予该权限,这样便能保证不可信代码不可能自行处理并吞噬该异常,从而保证宿主能正常的捕捉到ThreadAbortException异常,重新获取该线程的控制权,并把它重新放回到线程池中。

*当线程从它的ThreadAbortException展开时,不可信代码可以执行catch块的finally块,在这些块中代码可能进入无限循环从而组织宿主重新获取线程的控制权。这时候宿主应用程序可以用过卸载AppDomain、禁用CLR、终止进程的方式来修复这个问题。如果不可信代码捕捉了ThreadAbortException异常并且重新抛出一个新的异常类型,如果这个新的异常被捕捉到CLR会在catch的尾部自动重新抛出ThreadAbortException异常。

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 1月13日

程序集加载

加载过程

JIT编译器将IL代码编译成本地代码时,会查看IL代码引用了哪些类型。在运行时,JIT编译器利用程序集的TypeRef和AssemblyRef元数据表来确定哪一个程序集定义了所引用的类型。在AssemblyRef元数据表的记录项中,包含了构成程序集强名称的各个部分。JIT编译器获取所有这些部分:名称(无扩展名和路径)、版本、语言文化、公钥标记(如果被加载的程序集是弱命名的,那么标识中只包含:程序集的名称,将不再包含版本、语言文化、公钥标记信息),并把它们连接成一个字符串。然后CLR尝试将与该标识匹配的一个程序集加载到AppDomain中(如果还没加载的话)。在内部CLR通过System.Reflection.Assembly类的静态方法Load来尝试加载此程序集(可以显示的将一个程序集加载到AppDomain中)。

Assembly的Load方法
  • 加载强命名程序集

Load方法导致CLR向程序集应用一个版本绑定重定向策略,并在GAC(全局程序集缓存)中查找对应的程序集。如果没找到,就接着去应用程序的基目录、私有路径子目录和codebase位置查找。

  • 加载弱命名程序集

和加载强命名程序集不同的是,Load不会向应用程序应用版本绑定重定向策略,CLR也不会去GAC中查找程序集。

最后如果Load找到指定的程序集,就会返回已加载的程序集的一个Assembly对象的引用,如果没有加载到指定程序集,则会抛出一个System.IO.FileNotFoundException。

AppDomain的Load方法(尽量不使用)

与Assembly的Load方法不同,该方法属于实例方法。作用为将一个程序集加载到一个指定的AppDomain中。可供非托管代码调用,允许宿主将一个程序集注入一个指定的AppDomain。托管代码一般不使用该方法,因为在调用该方法后会开始在常规位置搜寻程序集,而AppDomain关联了一些设置告诉CLR如何查找程序集,为了加载程序集CLR将使用与指定AppDomain关联的设置,而不是发出调用的那个AppDomain关联的设置。以及AppDomain的Load方法返回对程序集的一个引用,由于System.Assembly类不是从System.MarshalByRefObject派生的,所以程序集必须按值封送回那个发起调用的AppDomain。现在CLR就会用发出调用的AppDomain的关联设置来定位并加载程序集。如果发出调用的AppDomain没有找到指定的程序集就会抛出FileNotFountException。这不是我们期望的行为,所以尽量避免使用AppDomain的Load方法。

Assembly的LoadFrom方法

在内部LoadFrom会先调用System.Reflection.AssemblyName类的静态方法GetAssemblyName,该方法打开指定的文件,查找AssemblyRef元数据表的记录项,提取程序集标识信息,然后返回System.Reflection.AssemblyName对象并关闭文件。随后LoadFrom方法在内部调用Assembly的Load方法,将AssemblyName对象传给它。接着CLR会应用版本绑定重定向策略,并在各个位置查找程序集。如果Load找到了匹配的程序集就会加载并返回已加载程序集的一个Assembly对象,LoadFrom方法将返回这个值。如果没有找到匹配的程序集,LoadFrom就会加载通过LoadFrom的实参传递的路径中的程序集。如果已加载了一个具有相同标识的程序集,LoadFrom方法会简单返回代表已加载程序集的一个Assembly对象。

*可以给LoadFrom方法传递一个URL实参,CLR会主动下载文件并将其安装到用户的下载缓存中,最后再从那加载文件

Assembly的LoadFile方法

调用这个方法可以从任意路径加载一个程序集,并可将具有相同标识的一个程序集多次加载到一个AppDomain中。通过LoadFile加载程序集时,CLR不会自动解析任何依赖问题,代码必须向AppDomain的AssemblyResolve事件登记,让事件回调方法显示加载任何依赖的程序集。

Assembly的ReflectionOnlyLoadFrom方法和ReflectionOnlyLoad方法
  • ReflectionOnlyLoadFrom:加载由路径指定的文件,文件的强名称标识不会获取,也不会再GAC和其它位置搜索文件。
  • ReflectionOnlyLoad:会在GAC、应用程序基目录、私有路径和codebase指定的位置搜索指定的程序集。和Load不同的是不会应用版本控制策略,即指定的什么加载版本,获得的便是哪个版本。
  • ReflectionOnlyLoadFrom和ReflectionOnlyLoad:这两个方法加载程序集时,CLR都会禁止程序集中的任何代码执行。试图执行这两个方法加载的程序集中的代码,都会导致CLR抛出InvalidOperationException。
存在多个具有相同标识的程序集

一台机器上可能同事存在多个具有相同标识的程序集,由于LoadFrom会在内部调用Load方法,所以CLR有可能不加载你指定的文件,而是加载一个不同的文件,从而达不到预期效果。建议每次生成程序集都修改版本号,确保每个版本都有自己的唯一标识,进而确保LoadFrom方法能达到预期行为。

程序集卸载

CLR并未提供单独卸载某个程序集的功能,因为一旦这种行为被允许,那么一旦线程从某个方法返回至已卸载的程序集中的代码,应用程序就会崩溃。要卸载程序集必须卸载程序集所在的整个AppDomain。而使用ReflectionOnlyLoadFrom方法和ReflectionOnlyLoad方法加载的程序集由于程序集中的代码不允许被执行,让人看上去这些程序集是可以卸载的,实际上CLR也不允许卸载这两个方法加载的程序集,因为即便不会运行程序集中的代码但仍然可以利用反射来创建对象,引用程序集中定义的元数据,如果程序集被卸载就必须使这些对象无效,从实现复杂度还是执行速度上来讲跟踪这些对象的状态都是得不偿失的。

查看原文

赞 1 收藏 0 评论 0

DoubleJ 发布了文章 · 1月4日

CLR寄宿

CLR寄宿

.NET Framework在Windows平台的顶部运行,意味着.NET Framework必须用Windows可以理解的技术构建:所有托管模块和程序集文件都必须使用Windows PE文件格式,要么是一个Windows EXE文件,要么是一个DLL文件。

CLRCreateInstance函数

定义在程序集MSCorEE.dll中,该程序集一般被成为垫片,该文件一般在C:\Windows\System32目录中。它的工作是负责决定创建哪个版本的CLR。

一台机器可以安装多个版本的CLR,但是只有一个版本的MSCorEE.dll文件。机器上安装的MSCorEE.dll版本是与安装的最新版的CLR一起发布的那个版本。

CLRCreateInstance函数可以返回一个ICLRMetaHost接口,可以调用该接口中的GetRuntime函数指定宿主要创建的CLR版本。然后垫片将所需版本的CLR加载到宿主的进程中。

GetRuntime函数返回一个指向非托管接口ICLRRuntimeInfo的指针,通过调用GetInterface方法获得ICLRRuntimeHost接口,可利用该接口做以下事情:

  • 设置宿主管理器:告诉CLR内存分配、线程调度/同步、程序集加载等决策。宿主还可以声明它想获得有关垃圾回收启动和停止以及特定操作超时的通知
  • 获取CLR管理器:告诉CLR阻止使用某些类/成员。宿主还能分辨哪些代码可以调式而哪些代码不能调试,以及一些特定事件(AppDomain卸载、堆栈溢出异常)发生时宿主应该调用哪些方法
  • 初始化并启动CLR
  • 加载一个程序集并执行其中的代码
  • 停止CLR,阻止任何更多的托管代码在Windows进程中运行

*Windows进程完全可以不加载CLR,只有需要在进程中执行托管代码时才需要加载它

CLR寄宿的好处
  • 可以用任何编程语言来编写
  • 代码在JIT编译后执行而不是一边解释一边执行,所以速度很快
  • 代码使用垃圾回收避免内存泄漏和损坏
  • 代码在一个安全的沙箱中运行
  • 宿主不必操心提供一个丰富的开发环境。宿主可以利用现有的技术,包括语言、编译器、编辑器、调试器、profiler等
查看原文

赞 1 收藏 0 评论 0

认证与成就

  • 获得 19 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-11-18
个人主页被 1.7k 人浏览