.NET|.NET Core对象池的应用:编程篇
目录
- 一、对象的借与还
- 二、依赖注入
- 三、池化对象策略
- 四、对象池的大小
- 五、对象的释放
【.NET|.NET Core对象池的应用:编程篇】
一、对象的借与还 和绝大部分的对象池编程方式一样,当我们需要消费某个对象的时候,我们不会直接创建它,而是选择从对象池中“借出”一个对象。一般来说,如果对象池为空,或者现有的对象都正在被使用,它会自动帮助我们完成对象的创建。借出的对象不再使用的时候,我们需要及时将其“归还”到对象池中以供后续复用。我们在使用.NET的对象池框架时,主要会使用如下这个ObjectPool
public abstract class ObjectPoolwhere 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
class Program{static async Task Main(){var objectPool = ObjectPool.Create(); 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
.NET提供的大部分框架都提供了针对IServiceCollection接口的扩展方法来注册相应的服务,但是对象池框架并没有定义这样的扩展方法,所以我们需要采用原始的方式来完成针对ObjectPoolProvider的注册。如下面的代码片段所示,在创建出ServiceCollection对象之后,我们通过调用AddSingleton扩展方法注册了ObjectPoolProvider的默认实现类型DefaultObjectPoolProvider。
class Program{static async Task Main(){var objectPool = new ServiceCollection().AddSingleton().BuildServiceProvider().GetRequiredService ().Create (); …}}
在利用ServiceCollection对象创建出代表依赖注入容器的IServiceProvider对象之后,我们利用它提取出ObjectPoolProvider对象,并通过调用其Create
三、池化对象策略 通过前面的实例演示可以看出,对象池在默认情况下会帮助我们完成对象的创建工作。我们可以想得到,它会在对象池无可用对象的时候会调用默认的构造函数来创建提供的对象。如果池化对象类型没有默认的构造函数呢?或者我们希望执行一些初始化操作呢?
在另一方面,当不在使用的对象被归还到对象池之前,很有可能会执行一些释放性质的操作(比如集合对象在归还之前应该被清空)。还有一种可能是对象有可能不能再次复用(比如它内部维护了一个处于错误状态并无法恢复的网络连接),那么它就不能被释放会对象池。上述的这些需求都可以通过IPooledObjectPolicy
同样以我们演示实例中使用的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
public class FoobarPolicy : IPooledObjectPolicy{public FoobarService Create() => FoobarService.Create(); public bool Return(FoobarService obj) => true; }
在调用ObjectPoolProvider对象的Create
class Program{static async Task Main(){var objectPool = new ServiceCollection().AddSingleton().BuildServiceProvider().GetRequiredService ().Create(new FoobarPolicy()); …}}
四、对象池的大小 对象池容纳对象的数量总归是有限的,默认情况下它的大小为当前机器处理器数量的2倍,这一点可以通过一个简单的实例来验证一下。如下面的代码片段所示,我们将演示程序中每次迭代并发执行ExecuteAsync方法的数量设置为当前机器处理器数量的2倍,并将最后一次创建的FoobarService对象的ID打印出来。为了避免控制台上的无效输出,我们将ExecuteAsync方法中的控制台输出代码移除。
class Program{static async Task Main(){var objectPool = new ServiceCollection().AddSingleton().BuildServiceProvider().GetRequiredService ().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().BuildServiceProvider().GetRequiredService ().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().BuildServiceProvider().GetRequiredService ().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对象池的应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
推荐阅读
- 数组常用方法一
- jQuery插件
- iOS面试题--基础
- 口红选得好,对象不愁找......
- 关于Core|关于Core Image
- java静态代理模式
- Python-类和对象
- ASP.NET|ASP.NET Core应用开发思维导图
- JavaScript|JavaScript: BOM对象 和 DOM 对象的增删改查
- JavaScript|JavaScript — call()和apply()、Date对象、Math、包装类、字符串的方法