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

目录
  • 一、对象的借与还
  • 二、依赖注入
  • 三、池化对象策略
  • 四、对象池的大小
  • 五、对象的释放

借助于有效的自动化垃圾回收机制,.NET让开发人员不在关心对象的生命周期,但实际上很多性能问题都来源于GC。并不说.NET的GC有什么问题,而是对象生命周期的跟踪和管理本身是需要成本的,不论交给应用还是框架来做,都会对性能造成影响。在一些对性能比较敏感的应用中,我们可以通过对象复用的方式避免垃圾对象的产生,进而避免GC因对象回收导致的性能损失。对象池是对象复用的一种常用的方式。.NET提供了一个简单高效的对象池框架,并使用在ASP.NET自身框架中。这个对象池狂框架由“Microsoft.Extensions.ObjectPool”这个NuGet包提供,我们可以通过添加这个NuGet包它引入我们的应用中。接下来我们就通过一些简单的示例来演示一下对象池的基本编程模式。

一、对象的借与还

和绝大部分的对象池编程方式一样,当我们需要消费某个对象的时候,我们不会直接创建它,而是选择从对象池中“借出”一个对象。一般来说,如果对象池为空,或者现有的对象都正在被使用,它会自动帮助我们完成对象的创建。借出的对象不再使用的时候,我们需要及时将其“归还”到对象池中以供后续复用。我们在使用.NET的对象池框架时,主要会使用如下这个ObjectPool<T>类型,针对池化对象的借与还体现在它的GetReturn方法中。

public abstract class ObjectPool<T> where T: class
{
    public abstract T Get();
    public abstract void Return(T obj);
}

我们接下来利用一个简单的控制台程序来演示对象池的基本编程模式。在添加了针对“Microsoft.Extensions.ObjectPool”这个NuGet包的引用之后,我们定义了如下这个FoobarService类型来表示希望池化复用的服务对象。如代码片段所示,FoobarService具有一个自增整数表示Id属性作为每个实例的唯一标识,静态字段_latestId标识当前分发的最后一个标识。

public class FoobarService
{
    internal static int _latestId;
    public int Id { get; }
    public FoobarService() => Id = Interlocked.Increment(ref _latestId);
}

通过对象池的方式来使用FoobarService对象体现在如下的代码片段中。我们通过调用ObjectPool类型的静态方法Create<FoobarService>方法得到针对FoobarService类型的对象池,这是一个ObjectPool<FoobarService>对象。针对单个FoobarService对象的使用体现在本地方法ExecuteAsync中。如代码片段所示,我们调用ObjectPool<FoobarService>对象的Get方法从对象池中借出一个Foobar对象。为了确定对象是否真的被复用,我们在控制台上打印出对象的标识。我们通过延迟1秒钟模拟针对服务对象的长时间使用,并在最后通过调用ObjectPool<FoobarService>对象的Return方法将借出的对象释放到对象池中。

class Program
{
    static async Task Main()
    {
        var objectPool = ObjectPool.Create<FoobarService>();
        while (true)
        {
            Console.Write("Used services: ");
            await Task.WhenAll(Enumerable.Range(1, 3).Select(_ => ExecuteAsync()));
            Console.Write("\n");
        }
        async Task ExecuteAsync()
        {
            var service = objectPool.Get();
            try
            {
                Console.Write($"{service.Id}; ");
                await Task.Delay(1000);
            }
            finally
            {
                objectPool.Return(service);
            }
        }
    }
}

在Main方法中,我们构建了一个无限循环,并在每次迭代中并行执行ExecuteAsync方法三次。演示实例运行之后会在控制台上输出如下所示的结果,可以看出每轮迭代使用的三个对象都是一样的。每次迭代,它们从对象池中被借出,使用完之后又回到池中供下一次迭代使用。

二、依赖注入

我们知道依赖注入是已经成为 .NET Core的基本编程模式,针对对象池的编程最好也采用这样的编程方式。如果采用依赖注入,容器提供的并不是代表对象池的ObjectPool<T>对象,而是一个ObjectPoolProvider对象。顾名思义, ObjectPoolProvider对象作为对象池的提供者,用来提供针对指定对象类型的ObjectPool<T>对象。

.NET提供的大部分框架都提供了针对IServiceCollection接口的扩展方法来注册相应的服务,但是对象池框架并没有定义这样的扩展方法,所以我们需要采用原始的方式来完成针对ObjectPoolProvider的注册。如下面的代码片段所示,在创建出ServiceCollection对象之后,我们通过调用AddSingleton扩展方法注册了ObjectPoolProvider的默认实现类型DefaultObjectPoolProvider

class Program
{
    static async Task Main()
    {
        var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create<FoobarService>();
        …
    }
}

在利用ServiceCollection对象创建出代表依赖注入容器的IServiceProvider对象之后,我们利用它提取出ObjectPoolProvider对象,并通过调用其Create<T>方法得到表示对象池的ObjectPool<FoobarService>对象。改动的程序执行之后同样会在控制台输出如上图所示的结果。

三、池化对象策略

通过前面的实例演示可以看出,对象池在默认情况下会帮助我们完成对象的创建工作。我们可以想得到,它会在对象池无可用对象的时候会调用默认的构造函数来创建提供的对象。如果池化对象类型没有默认的构造函数呢?或者我们希望执行一些初始化操作呢?

在另一方面,当不在使用的对象被归还到对象池之前,很有可能会执行一些释放性质的操作(比如集合对象在归还之前应该被清空)。还有一种可能是对象有可能不能再次复用(比如它内部维护了一个处于错误状态并无法恢复的网络连接),那么它就不能被释放会对象池。上述的这些需求都可以通过IPooledObjectPolicy<T>接口表示的池化对象策略来解决。

同样以我们演示实例中使用的FoobarService类型,如果并不希望用户直接调用构造函数来创建对应的实例,所以我们按照如下的方式将其构造函数改为私有,并定义了一个静态的工厂方法Create来创建FoobarService对象。当FoobarService类型失去了默认的无参构造函数之后,我们演示的程序将无法编译。

public class FoobarService
{
    internal static int _latestId;
    public int Id { get; }
    private FoobarService() => Id = Interlocked.Increment(ref _latestId);
    public static FoobarService Create() => new FoobarService();
}

为了解决这个问题,我们为FoobarService类型定义一个代表池化对象策略的FoobarPolicy类型。如代码片段所示,FoobarPolicy类型实现了IPooledObjectPolicy<FoobarService>接口,实现的Create方法通过调用FoobarSerivice类型的静态同名方法完成针对对象的创建。另一个方法Return可以用来执行一些对象归还前的释放操作,它的返回值表示该对象还能否回到池中供后续使用。由于FoobarService对象可以被无限次复用,所以实现的Return方法直接返回True。

public class FoobarPolicy : IPooledObjectPolicy<FoobarService>
{
    public FoobarService Create() => FoobarService.Create();
    public bool Return(FoobarService obj) => true;
}

在调用ObjectPoolProvider对象的Create<T>方法针对指定的类型创建对应的对象池的时候,我们将一个IPooledObjectPolicy<T>对象作为参数,创建的对象池将会根据该对象定义的策略来创建和释放对象。

class Program
{
    static async Task Main()
    {
        var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create(new FoobarPolicy());
         …
     }
}

四、对象池的大小

对象池容纳对象的数量总归是有限的,默认情况下它的大小为当前机器处理器数量的2倍,这一点可以通过一个简单的实例来验证一下。如下面的代码片段所示,我们将演示程序中每次迭代并发执行ExecuteAsync方法的数量设置为当前机器处理器数量的2倍,并将最后一次创建的FoobarService对象的ID打印出来。为了避免控制台上的无效输出,我们将ExecuteAsync方法中的控制台输出代码移除。

class Program
{
    static async Task Main()
    {
        var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create(new FoobarPolicy());
        var poolSize = Environment.ProcessorCount * 2;
        while (true)
        {
            while (true)
            {
                await Task.WhenAll(Enumerable.Range(1, poolSize).Select(_ => ExecuteAsync()));
                Console.WriteLine($"Last service: {FoobarService._latestId}");
            }
        }

        async Task ExecuteAsync()
        {
            var service = objectPool.Get();
            try
            {
                await Task.Delay(1000);
            }
            finally
            {
                objectPool.Return(service);
            }
        }
    }
}

上面这个演示实例表达的意思是:对象池的大小和对象消费率刚好是一致的。在这种情况下,消费的每一个对象都是从对象池中提取出来,并且能够成功还回去,那么对象的创建数量就是对象池的大小。下图所示的是演示程序运行之后再控制台上的输出结果,整个应用的生命周期范围内一共只会有16个对象被创建出来,因为我当前机器的处理器数量为8。

如果对象池的大小为当前机器处理器数量的2倍,那么我们倘若将对象的消费率提高,意味着池化的对象将无法满足消费需求,新的对象将持续被创建出来。为了验证我们的想法,我们按照如下的方式将每次迭代执行任务的数量加1。

class Program
{
    static async Task Main()
    {
        var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create(new FoobarPolicy());
        var poolSize = Environment.ProcessorCount * 2;
        while (true)
        {
            while (true)
            {
                await Task.WhenAll(Enumerable.Range(1, poolSize + 1)
                    .Select(_ => ExecuteAsync()));
                Console.WriteLine($"Last service: {FoobarService._latestId}");
            }
        }
        …
    }
}

再次运行改动后的程序,我们会在控制台上看到如下图所示的输出结果。由于每次迭代针对对象的需求量是17,但是对象池只能提供16个对象,所以每次迭代都必须额外创建一个新的对象。

五、对象的释放

由于对象池容纳的对象数量是有限的,如果现有的所有对象已经被提取出来,它会提供一个新创建的对象。从另一方面讲,我们从对象池得到的对象在不需要的时候总是会还回去,但是对象池可能容不下那么多对象,它只能将其丢弃,被丢弃的对象将最终被GC回收。如果对象类型实现了IDisposable接口,在它不能回到对象池的情况下,它的Dispose方法应该被立即执行。

为了验证不能正常回归对象池的对象能否被及时释放,我们再次对演示的程序作相应的修改。我们让FoobarService类型实现IDisposable接口,并在实现的Dispose方法中将自身ID输出到控制台上。然后我们按照如下的方式以每次迭代并发量高于对象池大小的方式消费对象。

class Program
{
    static async Task Main()
    {
        var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
            .BuildServiceProvider()
            .GetRequiredService<ObjectPoolProvider>()
            .Create(new FoobarPolicy());

        while (true)
        {
            Console.Write("Disposed services:");
            await Task.WhenAll(Enumerable.Range(1, Environment.ProcessorCount * 2 + 3).Select(_ => ExecuteAsync()));
            Console.Write("\n");
        }

        async Task ExecuteAsync()
        {
            var service = objectPool.Get();
            try
            {
                await Task.Delay(1000);
            }
            finally
            {
                objectPool.Return(service);
            }
        }
    }
}

public class FoobarService: IDisposable
{
    internal static int _latestId;
    public int Id { get; }
    private FoobarService() => Id = Interlocked.Increment(ref _latestId);
    public static FoobarService Create() => new FoobarService();
    public void Dispose() => Console.Write($"{Id}; ");
}

演示程序运行之后会在控制台上输出如下图所示的结果,可以看出对于每次迭代消费的19个对象,只有16个能够正常回归对象池,有三个将被丢弃并最终被GC回收。由于这样的对象将不能被复用,它的Dispose方法会被调用,我们定义其中的释放操作得以被及时执行。

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

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

(0)

相关推荐

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

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

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

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

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

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

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

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

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

  • Python设计模式编程中的备忘录模式与对象池模式示例

    Memento备忘录模式 备忘录模式一个最好想象的例子:undo! 它对对象的一个状态进行了'快照', 在你需要的时候恢复原貌.做前端会有一个场景:你设计一个表单,当点击提交会对表单内容 验证,这个时候你就要对用户填写的数据复制下来,当用户填写的不正确或者格式不对等问题, 就可以使用快照数据恢复用户已经填好的,而不是让用户重新来一遍,不是嘛? python的例子 这里实现了一个事务提交的例子 import copy def Memento(obj, deep=False): # 对你要做快照的对

  • 举例讲解Java设计模式中的对象池模式编程

    定义 一个对象池是一组已经初始化过且可以使用的对象的集合,池的用户可以从池子中取得对象,对其进行操作处理,并在不需要时归还给池子而非销毁它. 若初始化.实例化的代价高,且有需求需要经常实例化,但每次实例化的数量较少的情况下,使用对象池可以获得显著的效能提升.从池子中取得对象的时间是可预测的,但新建一个实例所需的时间是不确定. 实现 1. Reusable - 对象池中的对象,通常实例化代价比较高. 2. Client - 使用一个对象的实例. 3. ReusablePool - 管理对象的实例化

  • Java使用线程池实现socket编程的方法详解

    目录 前言 一.一个简单的C/S模型实现 1.服务器: 2.客户端: 二.线程池使用方法 1.新建一个线程池 2.用Runnable接口实现线程 3.创建线程对象并提交至线程池执行 三.结合起来 四.使用新的输入输出流 总结 前言 以多个客户端和一个服务端的socket通信为例,服务端启动时创建一个固定大小的线程池.服务端每接收到一个连接请求后(通信任务),交给线程池执行,任务类实现了Runnable接口,用于跟客户端进行读写操作,该类的对象作为任务通过execute(Runnable task

  • 对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

随机推荐