昨天在『.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 知道,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
卻不是同一個物件。
也就是說,當物件從池中取出超過指定容量的物件數量,雖然歸還了相同數量的物件,但物件池只允許容納 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
提供的物件池功能還是挺靈活的。普通場景使用使用預設的池化策略、預設的物件池和預設的物件池提供者就可以滿足需求,也可以自定義其中任意某部件來實現比較特殊或複雜的需求。
物件池的使用原則是:有借有還,再借不難。當呼叫元件從物件池中借走一個物件例項,使用完後應立即歸還給物件池,以便重複利用,避免因過多的物件初始化影響系統效能。