• .NET Core 对象池的使用


    昨天在『.NET 大牛之路』技术群和大家聊到了对象池的话题,今天展开详细讲讲这个知识点。

    这个概念大家都很熟悉,比如我们经常听到数据库连接池和线程池。它是一种基于使用预先分配资源集合的性能优化思想。

    简单说,对象池就是对象的容器,旨在优化资源的使用,通过在一个容器中池化对象,并根据需要重复使用这些池化对象来满足性能上的需求。当一个对象被激活时,便被从池中取出。当对象被停用时,它又被放回池中,等待下一个请求。对象池一般用于对象的初始化过程代价较大或使用频率较高的场景。

    那在 .NET 中如何实现或使用对象池呢?

    在 ASP.NET Core 框架里已经内置了一个对象池功能的实现:Microsoft.Extensions.ObjectPool。如果是控制台应用程序,可以单独安装这个扩展库。

    池化策略

    首先,要使用 ObjectPool,需要创建一个池化策略,告诉对象池你将如何创建对象,以及如何归还对象。

    该策略通过实现接口 IPooledObjectPolicy 来定义,下面是一个最简单的策略实现:

    public class FooPooledObjectPolicy : IPooledObjectPolicy<Foo>
    {
        public Foo Create()
        {
            return new Foo();
        }
    
        public bool Return(Foo obj)
        {
            return true;
        }
    }
    

    如果每次编码都要定义这样的策略,会比较麻烦,可以自己定义一个通用的泛型实现。Microsoft.Extensions.ObjectPool 中也提供了一个默认的泛型实现:DefaultPooledObjectPolicy<T>。如果不需要定义复杂的构造逻辑,使用默认的就行。下面我们来看看怎么使用。

    对象池的使用

    对象池使用的原则是:有借有还,再借不难。

    当对象池中没有实例时,则创建实例并返回给调用组件;当对象池中已有实例时,则直接取一个现有实例返回给调用组件。而且这个过程是线程安全的。

    Microsoft.Extensions.ObjectPool 提供了默认的对象池实现:DefaultObjectPool<T>,它提供了借 Get 和还 Return 操作接口。创建对象池时需要提供池化策略 IPooledObjectPolicy<T> 作为其构造参数。

    var policy = new DefaultPooledObjectPolicy<Foo>();
    var pool = new DefaultObjectPool<Foo>(policy);
    

    我们来看一个常规示例(C# 9.0 单文件完整代码):

    using Microsoft.Extensions.ObjectPool;
    using System;
    
    var policy = new DefaultPooledObjectPolicy<Foo>();
    var pool = new DefaultObjectPool<Foo>(policy);
    
    // 借
    var item1 = pool.Get();
    // 还
    pool.Return(item1);
    Console.WriteLine("item 1: {0}", item1.Id);
    
    // 借
    var item2 = pool.Get();
    // 还
    pool.Return(item2);
    Console.WriteLine("item 2: {0}", item2.Id);
    
    Console.ReadKey();
    
    public class Foo
    {
        public string Id { get; set; } = Guid.NewGuid().ToString("N");
    }
    

    打印结果:

    通过打印的 Id 知道,item1item2 是同一样对象。

    我们再来看看只借不还会是什么样子:

    // ...
    
    // 借
    var item1 = pool.Get();
    Console.WriteLine("item 1: {0}", item1.Id);
    
    // 再借
    var item2 = pool.Get();
    Console.WriteLine("item 2: {0}", item2.Id);
    
    // ...
    

    打印结果:

    可以看到,两个对象是不同的实例。所以,当调用组件从对象池中借走一个对象实例,使用完后应立即归还给对象池,以便重复使用,避免因构造新对象消耗过多资源。

    指定对象池容量

    在创建 DefaultObjectPool<T> 时,还可以指定第二个参数:对象池的容量。它表示最大可从该对象池取出的对象数量,指定数量以外的被取走的对象将不会被池化。我来演示一下,大家就知道什么意思了,请看示例:

    using Microsoft.Extensions.ObjectPool;
    using System;
    
    var policy = new DefaultPooledObjectPolicy<Foo>();
    
    // 指定容量为 2。
    var pool = new DefaultObjectPool<Foo>(policy, 2);
    
    // 借走 3 个
    var item1 = pool.Get();
    Console.WriteLine("item 1: {0}", item1.Id);
    var item2 = pool.Get();
    Console.WriteLine("item 2: {0}", item2.Id);
    var item3 = pool.Get();
    Console.WriteLine("item 3: {0}", item3.Id);
    
    // 再还会 3 个
    pool.Return(item1);
    pool.Return(item2);
    pool.Return(item3);
    
    
    // 再借走 3 个
    var item4 = pool.Get();
    Console.WriteLine("item 4: {0}", item4.Id);
    var item5 = pool.Get();
    Console.WriteLine("item 5: {0}", item5.Id);
    var item6 = pool.Get();
    Console.WriteLine("item 6: {0}", item6.Id);
    
    Console.ReadKey();
    

    注意示例代码中我给对象池指定了容量为 2,然后借走 3 个再归还 3 个,后面再借走 3 个。来看看打印结果:

    我们看到,item1item4 是同一个对象,item2item5 是同一个对象。item3item6 却不是同一个对象。

    也就是说,当对象从池中取出超过指定容量的对象数量,虽然归还了相同数量的对象,但对象池只允许容纳 2 个对象,第三个对象不会被池化。

    在 ASP.NET Core 中使用

    ASP.NET Core 框架内置好了 Microsoft.Extensions.ObjectPool,不需要单独安装。官方文档有个基于 ASP.NET Core 的使用示例:

    https://docs.microsoft.com/en-us/aspnet/core/performance/objectpool
    

    这个例子把 StringBuilder 做了池化。我这里就直接贴官方的例子了,为了更直观些,我把无关的代码简化掉了。

    先定义一个中间件:

    public class BirthdayMiddleware
    {
        private readonly RequestDelegate _next;
    
        public BirthdayMiddleware(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task InvokeAsync(HttpContext context, ObjectPool<StringBuilder> builderPool)
        {
            var stringBuilder = builderPool.Get();
            try
            {
                stringBuilder.Append("Hi");
                // 其它处理
                await context.Response.WriteAsync(stringBuilder.ToString());
            }
            finally // 即使出错也要保证归还对象
            {
                builderPool.Return(stringBuilder);
            }
        }
    }
    

    Startup 中注册相应的服务和中间件:

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
    
            services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
            {
                var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
                var policy = new StringBuilderPooledObjectPolicy();
                return provider.Create(policy);
            });
        }
    
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseMiddleware<BirthdayMiddleware>();
        }
    }
    

    这个示例用了 DefaultObjectPoolProvider,它是默认的对象池 Provider,所以你也可以自定义自己的对象池 Provider。

    总结

    对象池主要用在对象初始化比较耗时和使用比较频繁的场景,比如初始化时要读取网络资源,有时候这些对象因为有时效性,又不能用单例。

    Microsoft.Extensions.ObjectPool 提供的对象池功能还是挺灵活的。普通场景使用使用默认的池化策略、默认的对象池和默认的对象池提供者就可以满足需求,也可以自定义其中任意某部件来实现比较特殊或复杂的需求。

    对象池的使用原则是:有借有还,再借不难。当调用组件从对象池中借走一个对象实例,使用完后应立即归还给对象池,以便重复利用,避免因过多的对象初始化影响系统性能。

    作者:精致码农-王亮

    出处:http://cnblogs.com/willick

    联系:liam.wang@live.com

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如有问题或建议,请多多赐教,非常感谢。
  • 相关阅读:
    转载:MyBatis获取插入记录的自增长字段值
    006---抽象类
    005---组合
    004---继承与派生
    003---属性查找和绑定方法
    002---类与对象
    001---面向对象和面向过程的区别
    017---Django的中间件解决跨域
    10---git安装
    007---归并排序
  • 原文地址:https://www.cnblogs.com/willick/p/15000572.html
Copyright © 2020-2023  润新知