.NET Core对象池的应用:扩展篇

目录
  • 一、池化集合
  • 二、池化StringBuilder
  • 三、ArrayPool<T>
  • 四、MemoryPool<T>

原则上所有的引用类型对象都可以通过对象池来提供,但是在具体的应用中需要权衡是否值得用。虽然对象池能够通过对象复用的方式避免GC,但是它存储的对象会耗用内存,如果对象复用的频率很小,使用对象池是不值的。如果某个小对象的使用周期很短,能够确保GC在第0代就能将其回收,这样的对象其实也不太适合放在对象池中,因为第0代GC的性能其实是很高的。除此之外,对象释放到对象池之后就有可能被其他线程提取出来,如果释放的时机不对,有可能造成多个线程同时操作同一个对象。总之,我们在使用之前得考虑当前场景是否适用对象池,在使用的时候严格按照“有借有还”、“不用才还”的原则。

一、池化集合

我们知道一个List<T>对象内部会使用一个数组来保存列表元素。数组是定长的,所以List<T>有一个最大容量(体现为它的Capacity属性)。当列表元素数量超过数组容量时,必须对列表对象进行扩容,即创建一个新的数组并将现有的元素拷贝进去。当前元素越多,需要执行的拷贝操作就越多,对性能的影响自然就越大。如果我们创建List<T>对象,并在其中不断地添加对象,有可能会导致多次扩容,所以如果能够预知元素数量,我们在创建List<T>对象时应该指定一个合适的容量。但是很多情况下,列表元素数量是动态变化的,我们可以利用对象池来解决这个问题。

接下来我们通过一个简单的实例来演示一下如何采用对象池的方式来提供一个List<Foobar>对象,元素类型Foobar如下所示。为了能够显式控制列表对象的创建和归还,我们自定义了如下这个表示池化对象策略的FoobarListPolicy。通过《设计篇》针对对象池默认实现的介绍,我们知道直接继承PooledObjectPolicy<T>类型比实现IPooledObjectPolicy<T>接口具有更好的性能优势。

public class FoobarListPolicy : PooledObjectPolicy<List<Foobar>>
{
    private readonly int _initCapacity;
    private readonly int _maxCapacity;

    public FoobarListPolicy(int initCapacity, int maxCapacity)
    {
        _initCapacity = initCapacity;
        _maxCapacity = maxCapacity;
    }
    public override List<Foobar> Create() => new List<Foobar>(_initCapacity);
    public override bool Return(List<Foobar> obj)
   {
        if(obj.Capacity <= _maxCapacity)
        {
            obj.Clear();
            return true;
        }
        return false;
    }
}

public class Foobar
{
    public int Foo { get; }
    public int Bar { get; }
    public Foobar(int foo, int bar)
    {
        Foo = foo;
        Bar = bar;
    }
}

如代码片段所示,我们在FoobarListPolicy类型中定义了两个字段,_initCapacity字段表示列表创建时指定的初始容量,另一个_maxCapacity则表示对象池存储列表的最大容量。之所以要限制列表的最大容量,是为了避免复用几率很少的大容量列表常驻内存。在实现的Create方法中,我们利用初始容量创建出List<Foobar>对象。在Return方法中,我们先将待回归的列表清空,然后根据其当前容量决定是否要将其释放到对象池。下面的程序演示了采用对象池的方式来提供List<Foobar>列表。如代码片段所示,我们在调用ObjectPoolProvider对象的Create<T>创建代表对象池的ObjectPool<T>对象时,指定了作为池化对象策略的FoobarListPolicy对象。我们将初始和最大容量设置成1K(1024)和1M(1024*1024)。我们利用对象池提供了一个List<Foobar>对象,并在其中添加了10000个元素。如果这段代码执行的频率很高,对整体的性能是有提升的。

class Program
{
    static void Main()
    {
        var objectPool = new ServiceCollection()
            .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create(new FoobarListPolicy(1024, 1024*1024));

        string json;
        var list = objectPool.Get();
        try
        {
            list.AddRange(Enumerable.Range(1, 1000).Select(it => new Foobar(it, it)));
            json = JsonConvert.SerializeObject(list);
        }
        finally
        {
            objectPool.Return(list);
        }
    }
}

二、池化StringBuilder

我们知道,如果频繁涉及针对字符串拼接的操作,应该使用StringBuilder以获得更好的性能。实际上,StringBuilder对象自身也存在类似于列表对象的扩容问题,所以最好的方式就是利用对象池的方式来复用它们。对象池框架针对StringBuilder对象的池化提供的原生支持,我们接下来通过一个简单的示例来演示具体的用法。

class Program
{
    static void Main()
    {
        var objectPool = new ServiceCollection()
            .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .CreateStringBuilderPool(1024, 1024*1024);

        var builder = objectPool.Get();
        try
        {
            for (int index = 0; index < 100; index++)
            {
                builder.Append(index);
            }
            Console.WriteLine(builder);
        }
        finally
        {
            objectPool.Return(builder);
        }
    }
}

如上面的代码片段所示,我们直接可以调用ObjectPoolProvider的CreateStringBuilderPool扩展方法就可以得到针对StringBuilder的对象池对象(类型为ObjectPool<StringBuilder>)。我们上面演示实例一样,我们指定的也是StringBuilder对象的初始和最大容量。池化StringBuilder对象的核心体现在对应的策略类型上,即如下这个StringBuilderPooledObjectPolicy类型。

public class StringBuilderPooledObjectPolicy : PooledObjectPolicy<StringBuilder>
{
    public int InitialCapacity { get; set; } = 100;
    public int MaximumRetainedCapacity { get; set; } = 4 * 1024;

    public override StringBuilder Create()=> new StringBuilder(InitialCapacity);
    public override bool Return(StringBuilder obj)
    {
        if (obj.Capacity > MaximumRetainedCapacity)
        {
            return false;
        }
        obj.Clear();
        return true;
    }
}

可以看出它的定义和我们前面定义的FoobarListPolicy类型如出一辙。在默认情况下,池化StringBuilder对象的初始化和最大容量分别为100和5096。如下所示的是ObjectPoolProvider用于创建ObjectPool<StringBuilder>对象的两个CreateStringBuilderPool扩展方法的定义。

public static class ObjectPoolProviderExtensions
{
    public static ObjectPool<StringBuilder> CreateStringBuilderPool( this ObjectPoolProvider provider)
        => provider.Create(new StringBuilderPooledObjectPolicy());       

    public static ObjectPool<StringBuilder> CreateStringBuilderPool( this ObjectPoolProvider provider, int initialCapacity, int maximumRetainedCapacity)
    {
        var policy = new StringBuilderPooledObjectPolicy()
        {
            InitialCapacity = initialCapacity,
            MaximumRetainedCapacity = maximumRetainedCapacity,
        };
        return provider.Create(policy);
    }
}

三、ArrayPool<T>

接下来介绍的和前面的内容没有什么关系,但同属于我们常用对象池使用场景。我们在编程的时候会大量使用到集合,集合类型(像基于链表的集合除外)很多都采用一个数组作为内部存储,所以会有前面所说的扩容问题。如果这个数组很大,还会造成GC的压力。我们在前面已经采用池化集合的方案解决了这个问题,其实这个问题还有另外一种解决方案。

在很多情况下,当我们需要创建一个对象的时候,实际上需要的一段确定长度的连续对象序列。假设我们将数组对象进行池化,当我们需要一段定长的对象序列的时候,从池中提取一个长度大于所需长度的可用数组,并从中截取可用的连续片段(一般从头开始)就可以了。在使用完之后,我们无需执行任何的释放操作,直接将数组对象归还到对象池中就可以了。这种基于数组的对象池使用方式可以利用ArrayPool<T>来实现。

public abstract class ArrayPool<T>
{
    public abstract T[] Rent(int minimumLength);
    public abstract void Return(T[] array, bool clearArray);

    public static ArrayPool<T> Create();
    public static ArrayPool<T> Create(int maxArrayLength, int maxArraysPerBucket);

    public static ArrayPool<T> Shared { get; }
}

如上面的代码片段所示,抽象类型ArrayPool<T>同样提供了完成对象池两个基本操作的方法,其中Rent方法从对象池中“借出”一个不小于(不是等于)指定长度的数组,该数组最终通过Return方法释放到对象池。Return方法的clearArray参数表示在归还数组之前是否要将其清空,这取决我们针对数组的使用方式。如果我们每次都需要覆盖原始的内容,就没有必要额外执行这种多余操作。

我们可以通过静态方法Create创建一个ArrayPool<T>对象。池化的数组并未直接存储在对象池中,长度接近的多个数组会被封装成一个桶(Bucket)中,这样的好处是在执行Rent方法的时候可以根据指定的长度快速找到最为匹配的数组(大于并接近指定的长度)。对象池存储的是一组Bucket对象,允许的数组长度越大,桶的数量越多。Create方法除了可以指定数组允许最大长度,还可以指定每个桶的容量。除了调用静态Create方法创建一个独占使用的ArrayPool<T>对象之外,我们可以使用静态属性Shared返回一个应用范围内共享的ArrayPool<T>对象。ArrayPool<T>的使用非常方便,如下的代码片段演示了一个读取文件的实例。

class Program
{
    static async Task Main()
    {
        using var fs = new FileStream("test.txt", FileMode.Open);
        var length = (int)fs.Length;
        var bytes = ArrayPool<byte>.Shared.Rent(length);
        try
        {
            await fs.ReadAsync(bytes, 0, length);
            Console.WriteLine(Encoding.Default.GetString(bytes, 0, length));
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(bytes);
        }
    }
}

四、MemoryPool<T>

数组是对托管堆中用于存储同类对象的一段连续内存的表达,而另一个类型Memory<T>则具有更加广泛的应用,因为它不仅仅可以表示一段连续的托管(Managed)内存,还可以表示一段连续的Native内存,甚至线程堆栈内存。具有如下定义的MemoryPool<T>表示针对Memory<T>类型的对象池。

public abstract class MemoryPool<T> : IDisposable
{
    public abstract int MaxBufferSize { get; }
    public static MemoryPool<T> Shared { get; }

    public void Dispose();
    protected abstract void Dispose(bool disposing);
    public abstract IMemoryOwner<T> Rent(int minBufferSize = -1);
}

public interface IMemoryOwner<T> : IDisposable
{
    Memory<T> Memory { get; }
}

MemoryPool<T>和ArrayPool<T>具有类似的定义,比如通过静态属性Shared获取当前应用全局共享的MemoryPool<T>对象,通过Rent方法从对象池中借出一个不小于指定大小的Memory<T>对象。不同的是,MemoryPool<T>的Rent方法并没有直接返回一个Memory<T>对象,而是一个封装了该对象的IMemoryOwner<T>对象。MemoryPool<T>也没有定义一个用来释放Memory<T>对象的Reurn方法,这个操作是通过IMemoryOwner<T>对象的Dispose方法完成的。如果采用MemoryPool<T>,前面针对ArrayPool<T>的演示实例可以改写成如下的形式。

class Program
{
    static async Task Main()
    {
        using var fs = new FileStream("test.txt", FileMode.Open);
        var length = (int)fs.Length;
        using (var memoryOwner = MemoryPool<byte>.Shared.Rent(length))
        {
            await fs.ReadAsync(memoryOwner.Memory);
            Console.WriteLine(Encoding.Default.GetString( memoryOwner.Memory.Span.Slice(0,length)));
        }
    }
}

.NET Core对象池的应用:编程篇

.NET Core对象池的应用:设计篇

到此这篇关于.NET Core对象池的应用:扩展篇的文章就介绍到这了,更多相关.NET Core对象池的应用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • .NET Core对象池的应用:设计篇

    目录 一. IPooledObjectPolicy<T> 二.ObjectPool<T> DefaultObjectPool<T> DisposableObjectPool<T> 三.ObjectPoolProvider <编程篇>已经涉及到了对象池模型的大部分核心接口和类型.对象池模型其实是很简单的,不过其中有一些为了提升性能而刻意为之的实现细节倒是值得我们关注.总的来说,对象池模型由三个核心对象构成,它们分别是表示对象池的ObjectPool

  • .NET Core对象池的应用:编程篇

    目录 一.对象的借与还 二.依赖注入 三.池化对象策略 四.对象池的大小 五.对象的释放 借助于有效的自动化垃圾回收机制,.NET让开发人员不在关心对象的生命周期,但实际上很多性能问题都来源于GC.并不说.NET的GC有什么问题,而是对象生命周期的跟踪和管理本身是需要成本的,不论交给应用还是框架来做,都会对性能造成影响.在一些对性能比较敏感的应用中,我们可以通过对象复用的方式避免垃圾对象的产生,进而避免GC因对象回收导致的性能损失.对象池是对象复用的一种常用的方式..NET提供了一个简单高效的对

  • .NET Core中如何实现或使用对象池?

    目录 前言 池化策略 对象池的使用 指定对象池容量 在 ASP.NET Core 中使用 总结 前言 池这个概念大家都很熟悉,比如我们经常听到数据库连接池和线程池.它是一种基于使用预先分配资源集合的性能优化思想. 简单说,对象池就是对象的容器,旨在优化资源的使用,通过在一个容器中池化对象,并根据需要重复使用这些池化对象来满足性能上的需求.当一个对象被激活时,便被从池中取出.当对象被停用时,它又被放回池中,等待下一个请求.对象池一般用于对象的初始化过程代价较大或使用频率较高的场景. 那在 .NET

  • .NET Core对象池的应用:扩展篇

    目录 一.池化集合 二.池化StringBuilder 三.ArrayPool<T> 四.MemoryPool<T> 原则上所有的引用类型对象都可以通过对象池来提供,但是在具体的应用中需要权衡是否值得用.虽然对象池能够通过对象复用的方式避免GC,但是它存储的对象会耗用内存,如果对象复用的频率很小,使用对象池是不值的.如果某个小对象的使用周期很短,能够确保GC在第0代就能将其回收,这样的对象其实也不太适合放在对象池中,因为第0代GC的性能其实是很高的.除此之外,对象释放到对象池之后就

  • ASP.NET Core中的对象池介绍

    asp.net core中通过扩展库的方式提供给了一个标准的对象池ObjectPool,定义在Microsoft.Extensions.ObjectPool.dll 程序集中.它本身是个纯虚的抽象类,它就定义了两个接口函数,实现如下 public abstract class ObjectPool<T> where T : class { public abstract T Get(); public abstract void Return(T obj); } 这是一个比较典型的对象池接口:

  • 一文搞懂Java中对象池的实现

    目录 1. 什么是对象池 2. 为什么需要对象池 3. 对象池的实现 4. 开源的对象池工具 5. JedisPool 对象池实现分析 6. 对象池总结 最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 8ms 左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有 0.01ms,这篇文章介绍对象池相关知识. 1. 什么是对象池 池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以

  • .NET Core 中对象池 Object Pool的使用

    目录 一.什么是对象池 二..NET Core 中的对象池 三.本文小结 一.什么是对象池 对象池简单来说就是一种为对象提供可复用能力的软件设计思路.我们常说有借有还,再借不难,而对象池就是通过借和还这样两个动作来保证对象可以被重复使用,从而节省频繁创建对象的性能开销.对象池最常用的场景是游戏设计,因为在游戏中大量存在着可复用的对象,源源不断的子弹出现并不是循环再生的.在数据库中存在着被称为连接池的东西,每当出现数据库无法连接的情况时,经验丰富的开发人员往往会先检查连接池是否满了,这其实就是对象

  • 基于Apache组件分析对象池原理的实现案例分析

    目录 一.设计与原理 1.基础案例 2.接口设计 1.1 PooledObjectFactory 接口 1.2 ObjectPool 接口 1.3 PooledObject 接口 3.运行原理 二.构造分析 1.对象池 2.双端队列 三.对象管理 1.添加对象 2.借用对象 3.归还对象 4.对象状态 四.Redis应用 1.配置管理 2.源码分析 2.1 配置转换 2.2 对象池构造 2.3 对象管理 五.参考源码 池塘里养:Object: 一.设计与原理 1.基础案例 首先看一个基于comm

  • 对Python中小整数对象池和大整数对象池的使用详解

    1. 小整数对象池 整数在程序中的使用非常广泛,Python为了优化速度,使用了小整数对象池, 避免为整数频繁申请和销毁内存空间. Python 对小整数的定义是 [-5, 256] 这些整数对象是提前建立好的,不会被垃圾回收.在一个 Python 的程序中,无论这个整数处于LEGB中的哪个位置, 所有位于这个范围内的整数使用的都是同一个对象.同理,单个字母也是这样的. In [1]: a=-5 In [2]: b=-5 In [3]: a is b Out[3]: True In [4]: a

  • 7分钟读懂Go的临时对象池pool以及其应用场景

    临时对象池 pool 是啥? sync.Pool 给了一大段注释来说明 pool 是啥,我们看看这段都说了些什么. 临时对象池是一些可以分别存储和取出的临时对象. 池中的对象会在没有任何通知的情况下被移出(释放或者重新取出使用).如果 pool 中持有某个对象的唯一引用,则该对象很可能会被回收. Pool 在多 goroutine 使用环境中是安全的. Pool 是用来缓存已经申请了的 目前未使用的 接下来可能会使用的 内存,以此缓解 GC 压力.使用它可以方便高效的构建线程安全的 free l

  • 深度剖析C++对象池自动回收技术实现

    对象池可以显著提高性能,如果一个对象的创建非常耗时或非常昂贵,频繁去创建的话会非常低效.对象池通过对象复用的方式来避免重复创建对象,它会事先创建一定数量的对象放到池中,当用户需要创建对象的时候,直接从对象池中获取即可,用完对象之后再放回到对象池中,以便复用.这种方式避免了重复创建耗时或耗资源的大对象,大幅提高了程序性能.本文将探讨对象池的技术特性以及源码实现. 对象池类图 ObjectPool:管理对象实例的pool. Client:使用者. 适用性: 类的实例可重用. 类的实例化过程开销较大.

随机推荐