池这个概念大家都很熟悉,比如我们经常听到数据库连接池和线程池。它是一种基于使用预先分配资源集合的性能优化思想。
简单说,对象池就是对象的容器,旨在优化资源的使用,通过在一个容器中池化对象,并根据需要重复使用这些池化对象来满足性能上的需求。当一个对象被激活时,便被从池中取出。当对象被停用时,它又被放回池中,等待下一个请求。对象池一般用于对象的初始化过程代价较大或使用频率较高的场景。
那在 .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 知道,item1 和 item2 是同一样对象。
我们再来看看只借不还会是什么样子:
// ... // 借 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 个。来看看打印结果:
我们看到,item1 与 item4 是同一个对象,item2 与 item5 是同一个对象。item3 与 item6 却不是同一个对象。