我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

.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对象池的应用内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

.NET Core中对象池Object Pool的使用方法是什么

这篇文章主要讲解了“.NET Core中对象池Object Pool的使用方法是什么”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“.NET Core中对象池Object Pool的使用方法是
2023-06-25

Golang 函数在面向对象编程中的应用

go 函数可作为对象的方法使用。方法是与对象关联的函数,可访问对象的字段和方法。在 go 中,使用 func (receiver_type) identifier(parameters) return_type 语法定义方法。这种方法提供了
Golang 函数在面向对象编程中的应用
2024-05-23

C++ 多线程编程中线程池的应用

c++++ 多线程编程中使用线程池的好处包括:1)减少线程创建次数;2)负载均衡;3)避免资源争用。例如,通过使用线程池将图像转换任务分配给线程池,可以提高文件转换应用程序的转换速度。C++ 多线程编程中线程池的应用在现代 C++ 应用程
C++ 多线程编程中线程池的应用
2024-05-14

ASP Core 中的异步编程:编写响应迅速的 Web 应用程序

在 ASP Core 中运用异步编程模式,可以显著提升 Web 应用程序的响应速度,为用户提供更流畅、更高效的使用体验。
ASP Core 中的异步编程:编写响应迅速的 Web 应用程序
2024-03-07

面向对象编程中 C++ 内联函数的应用

内联函数是在 oop 中提高方法调用效率的有用工具,因为它们在编译时展开,避免了函数调用的开销。声明内联函数时,在函数定义前添加 inline 关键字即可。内联函数的优点包括提高性能、减小代码大小和提高可读性。但在使用时,需注意潜在的代码膨
面向对象编程中 C++ 内联函数的应用
2024-04-16

STL 函数对象在处理并发编程中的应用?

在并发编程中,stl 函数对象可以通过以下应用简化并行处理:并行任务处理:封装函数对象为可并行执行的任务。队列处理:存储函数对象,并将它们调度到不同线程。事件处理:将函数对象注册为事件侦听器,在触发事件时执行。STL 函数对象在处理并发编程
STL 函数对象在处理并发编程中的应用?
2024-04-25

Golang函数的优势在面向对象的编程中的应用?

go 函数在面向对象编程中提供了以下优势:函数式编程,支持一等值和高阶函数;对象封装,将数据和行为保存在一个结构体内;代码复用,创建通用函数供不同上下文中重用;并发编程,使用 goroutine 和 channel 管理并发代码。在面向对象
Golang函数的优势在面向对象的编程中的应用?
2024-04-11

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录