原则上所有的引用类型对象都可以通过对象池来提供,但是在具体的应用中需要权衡是否值得用。虽然对象池能够通过对象复用的方式避免GC,但是它存储的对象会耗用内存,如果对象复用的频率很小,使用对象池是不值的。如果某个小对象的使用周期很短,能够确保GC在第0代就能将其回收,这样的对象其实也不太适合放在对象池中,因为第0代GC的性能其实是很高的。除此之外,对象释放到对象池之后就有可能被其他线程提取出来,如果释放的时机不对,有可能造成多个线程同时操作同一个对象。总之,我们在使用之前得考虑当前场景是否适用对象池,在使用的时候严格按照“有借有还”、“不用才还”的原则。
一、池化集合我们知道一个List<T>对象内部会使用一个数组来保存列表元素。数组是定长的,所以List<T>有一个最大容量(体现为它的Capacity属性)。当列表元素数量超过数组容量时,必须对列表对象进行扩容,即创建一个新的数组并将现有的元素拷贝进去。当前元素越多,需要执行的拷贝操作就越多,对性能的影响自然就越大。如果我们创建List<T>对象,并在其中不断地添加对象,有可能会导致多次扩容,所以如果能够预知元素数量,我们在创建List<T>对象时应该指定一个合适的容量。但是很多情况下,列表元素数量是动态变化的,我们可以利用对象池来解决这个问题。
接下来我们通过一个简单的实例来演示一下如何采用对象池的方式来提供一个List<Foobar>对象,元素类型Foobar如下所示。为了能够显式控制列表对象的创建和归还,我们自定义了如下这个表示池化对象策略的FoobarListPolicy。通过《设计篇》针对对象池默认实现的介绍,我们知道直接继承PooledObjectPolicy<T>类型比实现IPooledObjectPolicy<T>接口具有更好的性能优势。
public class FoobarListPolicy : PooledObjectPolicy<List<Foobar>> { private readonly int _initCapacity; private readonly int _maxCapacity; public FoobarListPolicy(int initCapacity, int maxCapacity) { _initCapacity = initCapacity; _maxCapacity = maxCapacity; } public override List<Foobar> Create() => new List<Foobar>(_initCapacity); public override bool Return(List<Foobar> obj) { if(obj.Capacity <= _maxCapacity) { obj.Clear(); return true; } return false; } } public class Foobar { public int Foo { get; } public int Bar { get; } public Foobar(int foo, int bar) { Foo = foo; Bar = bar; } }
如代码片段所示,我们在FoobarListPolicy类型中定义了两个字段,_initCapacity字段表示列表创建时指定的初始容量,另一个_maxCapacity则表示对象池存储列表的最大容量。之所以要限制列表的最大容量,是为了避免复用几率很少的大容量列表常驻内存。在实现的Create方法中,我们利用初始容量创建出List<Foobar>对象。在Return方法中,我们先将待回归的列表清空,然后根据其当前容量决定是否要将其释放到对象池。下面的程序演示了采用对象池的方式来提供List<Foobar>列表。如代码片段所示,我们在调用ObjectPoolProvider对象的Create<T>创建代表对象池的ObjectPool<T>对象时,指定了作为池化对象策略的FoobarListPolicy对象。我们将初始和最大容量设置成1K(1024)和1M(1024*1024)。我们利用对象池提供了一个List<Foobar>对象,并在其中添加了10000个元素。如果这段代码执行的频率很高,对整体的性能是有提升的。
class Program { static void Main() { var objectPool = new ServiceCollection() .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .Create(new FoobarListPolicy(1024, 1024*1024)); string json; var list = objectPool.Get(); try { list.AddRange(Enumerable.Range(1, 1000).Select(it => new Foobar(it, it))); json = JsonConvert.SerializeObject(list); } finally { objectPool.Return(list); } } }
二、池化StringBuilder我们知道,如果频繁涉及针对字符串拼接的操作,应该使用StringBuilder以获得更好的性能。实际上,StringBuilder对象自身也存在类似于列表对象的扩容问题,所以最好的方式就是利用对象池的方式来复用它们。对象池框架针对StringBuilder对象的池化提供的原生支持,我们接下来通过一个简单的示例来演示具体的用法。
class Program { static void Main() { var objectPool = new ServiceCollection() .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .CreateStringBuilderPool(1024, 1024*1024); var builder = objectPool.Get(); try { for (int index = 0; index < 100; index++) { builder.Append(index); } Console.WriteLine(builder); } finally { objectPool.Return(builder); } } }