zoukankan      html  css  js  c++  java
  • 对象池与.net—从一个内存池实现说起

    本来想写篇关于System.Collections.Immutable中提供的ImmutableList里一些实现细节来着,结果一时想不起来源码在哪里——为什么会变成这样呢……第一次有了想写分析的源码,又有了写博客的时间。两件快乐事情重合在一起。而这两份快乐,又给我带来更多的快乐。得到的,本该是像梦境一般幸福的时间……但是,为什么,会变成这样呢……还好顺路看到MS开源的一个基于内存池的MemoryStream替代实现,看起来用这个水一篇文章妥妥的。

    ps: 虽然在标题上扯了.net,不过说实话除了代码是用c#外,我自己也想不出来其中的技术概念与.net有几毛钱关系。如果愿意的话,
    绝大部分语言都可以实现来着。

    var ms = new MemoryStream()

    在讨论对象池之前,咱们先来看一段简单的代码。

    var ms = new MemoryStream();
    DataContractSerializer serializer = new DataContractSerializer(typeof(Model));
    serializer.WriteObject(ms, model);
    

    这段代码采用DataContractSerializer序列化一个类型为Model的对象,没有什么特殊的技巧可言。大部分情况下也没必要去折腾这块代码。

    不过,在实际系统中,此处还是存在着一个不大不小的问题——每次运行这个代码段都会新建一个MemoryStream,然后往这个MemoryStream中写入字节流。

    看上去MemoryStream非常神奇,它是一个基于内存,可以不断写入(只要还有内存)的流。不过如果查看源码,就会发现它实际上还是基于byte[]实现的,每次扩展容量时,都新建一个足够大的byte[]

    if (_expandable && value != _capacity) {
        if (value > 0) {
            byte[] newBuffer = new byte[value];
            if (_length > 0) Buffer.InternalBlockCopy(_buffer, 0, newBuffer, 0, _length);
            _buffer = newBuffer;
        }
        else {
            _buffer = null;
        }
        _capacity = value;
    }
    

    别的不说,默默新建的那一堆byte[]对GC产生的压力,甚至有可能触发过多的gen2 gc,对提高系统的吞吐量都是非常不利的。最好能重复利用已经分配出来的内存空间,让这些byte[]活的越久越好。于是我们就得把用完的MemoryStream回收回来,洗洗更健康再用。

    对象池

    其实对象池的基本思路非常简单,无非分为以下几步:

    1. 创建池
    2. 使用者向池申请资源
    3. 池分配可用的对象(如果没有现成的,则创建个新对象)
    4. 使用者用完后将对象还回池中

    无论是ADO.NET中的连接池,还是ThreadPool线程池,抑或接下来要说到的RecyclableMemoryStream内存池,都离不开
    这个思路。

    下载源码

    由于原版的RecyclableMemoryStream中的功能完整,不太适合讲解对象池的原理之用,故特地裁剪了个
    版本出来——虽然也能用,不过在实际项目中还是推荐用nuget安装完整版的。

    简化版请点此处

    static RecyclableMemoryStreamManager recyclableMemoryManager = new Microsoft.IO.RecyclableMemoryStreamManager()

    对照对象池的思路,码农说:“要有池。”就有了池。
    码农看池是全局的,就把池赋给了静态字段。
    码农称池为recyclableMemoryManager,这是头一步。

    唯一重要的就是下面这行代码

    this.smallPool = new ConcurrentStack<byte[]>();
    

    ………………只是简单的建立了个栈来管理内存块………………

    对了,前面忘记补充说明下,为了充分管理内存的使用,RecyclableMemoryStream中并不是直接将MemoryStream
    管理起来,而是另外实现了个MemoryStream,并改为由池来管理固定大小的byte[]内存块。
    此处的smallPool就是核心的内存块池。

    var ms = reusableMemoryStreamManager.GetStream()

    接下来就是使用者向池申请资源了——才怪!

    由于RecyclableMemoryStream的实现是基于内存块管理的思路,故而创建一个新的RecyclableMemoryStream
    实例不需要特别做什么。

    ReusableMemoryStream.EnsureCapacity()

    接下来就是使用者向池申请资源了。无论是向流中写入数据,还是通过CapacitySetLength方法
    显示设置流的长度,最终都需要确保流中有足够的内存空间。废话不说看源码:

    private void EnsureCapacity(int newCapacity)
    {
        while (this.Capacity < newCapacity)
        {
            blocks.Add((this.memoryManager.GetBlock()));
        }
    }
    

    这里的blocks是一个List<byte[]>,用来存储已分配到的内存块。memoryManagerRecyclableMemoryStreamManager类型的对象池——
    也就是最开始我们创建的recyclableMemoryManager啦。

    RecyclableMemoryStreamManager.GetBlock()

    前面EnsureCapacity方法的关键就是调用GetBlock方法获取内存块——也就是对象池分配可用的对象。继续废话不说上代码

    internal byte[] GetBlock()
    {
        byte[] block;
        if (!this.smallPool.TryPop(out block))
        {
            // We'll add this back to the pool when the stream is disposed
            // (unless our free pool is too large)
            block = new byte[this.BlockSize];
        }
        return block;
    }
    

    别说我偷懒,如此简单的代码还需要解释么?

    ReusableMemoryStream.Dispose()

    上面偷懒我认了,接下来我坚决不偷懒——我要贴两块!

    使用者用完后将对象还回池中,通常需要用户释放资源——.Net下通常意味着实现IDisposable接口。

    ReusableMemoryStream的Dispose方法写的比较完整,如果对Dispose方法的正确姿势没有研究的话推荐看看。
    但是话说回来,咱们现在讨论的是对象池,所以关键的代码只有一行:

    this.memoryManager.ReturnBlocks(this.blocks);
    

    而RecyclableMemoryStreamManager的ReturnBlocks方法关键代码如下:

    
    本来想写篇关于System.Collections.Immutable中提供的ImmutableList里一些实现细节来着,
    结果一时想不起来源码在哪里——为什么会变成这样呢……第一次有了想写分析的源码,又有了写博客的时间。
    两件快乐事情重合在一起。而这两份快乐,又给我带来更多的快乐。得到的,本该是像梦境一般幸福的时间……
    但是,为什么,会变成这样呢……还好顺路看到MS开源的一个基于内存池的MemoryStream替代实现,看起来用这个水一篇文章妥妥的。
    
    ps: 虽然在标题上扯了.net,不过说实话除了代码是用c#外,我自己也想不出来其中的技术概念与.net有几毛钱关系。如果愿意的话,
    绝大部分语言都可以实现来着。
    
    ## var ms = new MemoryStream()
    
    在讨论对象池之前,咱们先来看一段简单的代码。
    
    ```c#
    var ms = new MemoryStream();
    DataContractSerializer serializer = new DataContractSerializer(typeof(Model));
    serializer.WriteObject(ms, model);
    

    这段代码采用DataContractSerializer序列化一个类型为Model的对象,没有什么特殊的技巧可言。大部分情况下也没必要去折腾这块代码。

    不过,在实际系统中,此处还是存在着一个不大不小的问题——每次运行这个代码段都会新建一个MemoryStream,
    然后往这个MemoryStream中写入字节流。

    看上去MemoryStream非常神奇,它是一个基于内存,可以不断写入(只要还有内存)的流。不过如果查看
    源码,就会发现它实际上还是基于byte[]实现的,每次扩展容量时,都新建一个足够大byte[]

    if (_expandable && value != _capacity) {
        if (value > 0) {
            byte[] newBuffer = new byte[value];
            if (_length > 0) Buffer.InternalBlockCopy(_buffer, 0, newBuffer, 0, _length);
            _buffer = newBuffer;
        }
        else {
            _buffer = null;
        }
        _capacity = value;
    }
    

    别的不说,默默新建的那一堆byte[]对GC产生的压力,甚至有可能触发过多的gen2 gc
    对提高系统的吞吐量都是非常不利的。最好能重复利用已经分配出来的内存空间,让这些byte[]活的越久越好。于是我们就得把用完的MemoryStream回收回来,
    洗洗更健康再用。

    对象池

    其实对象池的基本思路非常简单,无非分为以下几步:

    1. 创建池
    2. 使用者向池申请资源
    3. 池分配可用的对象(如果没有现成的,则创建个新对象)
    4. 使用者用完后将对象还回池中

    无论是ADO.NET中的连接池,还是ThreadPool线程池,抑或接下来要说到的RecyclableMemoryStream内存池,都离不开这个思路。

    下载源码

    由于原版的RecyclableMemoryStream中的功能完整,不太适合讲解对象池的原理之用,故特地裁剪了个版本出来——虽然也能用,不过在实际项目中还是推荐用nuget安装完整版的。

    简化版请点此处

    static RecyclableMemoryStreamManager recyclableMemoryManager = new Microsoft.IO.RecyclableMemoryStreamManager()

    对照对象池的思路,码农说:“要有池。”就有了池。
    码农看池是全局的,就把池赋给了静态字段。
    码农称池为recyclableMemoryManager,这是头一步。

    唯一重要的就是下面这行代码

    this.smallPool = new ConcurrentStack<byte[]>();
    

    ………………只是简单的建立了个栈来管理内存块………………

    对了,前面忘记补充说明下,为了充分管理内存的使用,RecyclableMemoryStream中并不是直接将MemoryStream管理起来,而是另外实现了个MemoryStream,并改为由池来管理固定大小的byte[]内存块。此处的smallPool就是核心的内存块池。

    var ms = reusableMemoryStreamManager.GetStream()

    接下来就是使用者向池申请资源了——才怪!

    由于RecyclableMemoryStream的实现是基于内存块管理的思路,故而创建一个新的RecyclableMemoryStream实例时不需要特别做什么。

    ReusableMemoryStream.EnsureCapacity()

    接下来就是使用者向池申请资源了。无论是向流中写入数据,还是通过CapacitySetLength方法显式设置流的长度,最终都需要确保流中有足够的内存空间。废话不说看源码:

    private void EnsureCapacity(int newCapacity)
    {
        while (this.Capacity < newCapacity)
        {
            blocks.Add((this.memoryManager.GetBlock()));
        }
    }
    

    这里的blocks是一个List<byte[]>,用来存储已分配到的内存块。memoryManagerRecyclableMemoryStreamManager类型的对象池——
    也就是最开始我们创建的recyclableMemoryManager啦。

    RecyclableMemoryStreamManager.GetBlock()

    前面EnsureCapacity方法的关键就是调用GetBlock方法获取内存块——也就是对象池分配可用的对象。继续废话不说上代码

    internal byte[] GetBlock()
    {
        byte[] block;
        if (!this.smallPool.TryPop(out block))
        {
            // We'll add this back to the pool when the stream is disposed
            // (unless our free pool is too large)
            block = new byte[this.BlockSize];
        }
        return block;
    }
    

    别说我偷懒,如此简单的代码还需要解释么?

    ReusableMemoryStream.Dispose()

    上面偷懒我认了,接下来我坚决不偷懒——我要贴两块!

    使用者用完后将对象还回池中,通常需要用户释放资源——.Net下通常意味着实现IDisposable接口。

    ReusableMemoryStream的Dispose方法写的比较完整,如果对Dispose方法的正确姿势没有研究的话推荐看看。但是话说回来,咱们现在讨论的是对象池,所以关键的代码只有一行:

    this.memoryManager.ReturnBlocks(this.blocks);
    

    而RecyclableMemoryStreamManager的ReturnBlocks方法关键代码如下:

    foreach (var block in blocks)
    {
        this.smallPool.Push(block);
    }
    

    到这里,内存块又回归内存池之海了(又是池又是海的……你要理解!理解!)。

    结束语

    只要理解了对象池技术的原理,就会发现这个技术一点都不复杂——虽然实际工程中可能还需要编写大量的代码。其实很多听上去玄乎的技术,解释开了也不过如此。

    Adiós

  • 相关阅读:
    自习任我行第二阶段个人总结9
    自习任我行第二阶段个人总结8
    自习任我行第二阶段个人总结7
    自习任我行第二阶段个人总结6
    自习任我行第二阶段个人总结5
    自习任我行 第二阶段每日个人总结4
    自习任我行 第二阶段每日个人总结3
    自习任我行 第二阶段每日个人总结2
    自习任我行 第二阶段每日个人总结1
    结课总结
  • 原文地址:https://www.cnblogs.com/Nyarlathotep/p/5399825.html
Copyright © 2011-2022 走看看