C#.NET ObjectPool 對象復(fù)用、池化策略與使用邊界
簡介
在 .NET 里做性能優(yōu)化時,很多人第一反應(yīng)是:
- 少分配
- 少
GC - 少臨時對象
這個方向本身沒有問題。
但問題在于,優(yōu)化一旦開始,很容易走偏成另外一種極端:
- 看到對象創(chuàng)建就想池化
- 覺得池化一定比
new更快 - 把對象池當(dāng)成一種“萬能緩存”
ObjectPool 這類工具,真正值錢的地方不是“把所有對象都放進(jìn)池里”,而是:
對某些創(chuàng)建成本不算低、使用頻率高、生命周期短、又能安全復(fù)用的對象,減少重復(fù)分配和回收的成本。
這篇文章重點講清楚幾件事:
ObjectPool到底是什么;- 為什么會有它;
.NET里的官方實現(xiàn)怎么用;- 怎么安裝;
- 一個最小 demo 怎么跑;
- 自定義池化對象怎么寫;
- 它適合什么場景,不適合什么場景。
一句話先給結(jié)論:
ObjectPool 不是為了替代正常對象創(chuàng)建,而是為了在特定熱點路徑里復(fù)用“可重置的短期對象”。
ObjectPool到底是什么?
它位于:
Microsoft.Extensions.ObjectPool
核心抽象很簡單:
ObjectPool<T>
最重要的兩個動作也很簡單:
Get():從池里取一個對象Return(obj):把對象還回池里
它的思路不是:
- 預(yù)先把所有對象都準(zhǔn)備好
- 永遠(yuǎn)保證對象不創(chuàng)建
而更像:
- 能復(fù)用就復(fù)用
- 復(fù)用不了就新建
- 還回來時如果池里放不下,也可以直接丟掉
所以一開始就要建立一個正確心智模型:
對象池是“盡量復(fù)用”,不是“絕不分配”。
為什么會有它?
先看一個最典型的例子:
for (var i = 0; i < 100_000; i++)
{
var sb = new StringBuilder();
sb.Append("hello");
_ = sb.ToString();
}
這段代碼邏輯上沒問題,但如果它出現(xiàn)在高頻路徑里,就會帶來很直接的成本:
- 持續(xù)分配對象
- 增加
GC壓力 - 熱點路徑上吞吐變差
如果這個對象滿足幾件事:
- 創(chuàng)建本身有一定成本
- 用完之后能清理干凈
- 不會被跨線程亂共享
- 高頻重復(fù)出現(xiàn)
那就有池化的價值。
所以 ObjectPool 解決的不是“對象太多”這么寬泛的問題,而是更具體的這一類問題:
- 某些對象很適合復(fù)用
- 反復(fù)創(chuàng)建和回收它們不劃算
它和緩存、連接池、數(shù)組池是什么關(guān)系?
這幾個概念經(jīng)常被混在一起,但其實不是一回事。
1. 和緩存不一樣
緩存關(guān)心的是:
- 數(shù)據(jù)值能不能復(fù)用
- 下次還能不能直接命中
對象池關(guān)心的是:
- 這個對象實例能不能再利用
也就是說,緩存復(fù)用的是“結(jié)果”,對象池復(fù)用的是“殼”。
2. 和數(shù)據(jù)庫連接池不一樣
連接池里的資源通常更重,而且?guī)в型獠肯到y(tǒng)狀態(tài)。
對象池更常見的目標(biāo)通常是:
StringBuilder- 序列化緩沖對象
- 臨時 parser
- 可重置的上下文對象
3. 和ArrayPool<T>不一樣
ArrayPool<T> 復(fù)用的是數(shù)組緩沖區(qū)。
ObjectPool<T> 復(fù)用的是一個完整對象。
兩者都屬于“減少分配”的工具,但粒度不同。
安裝
官方包是:
dotnet add package Microsoft.Extensions.ObjectPool
如果你習(xí)慣看 csproj,對應(yīng)就是:
<ItemGroup> <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="*" /> </ItemGroup>
常用命名空間:
using Microsoft.Extensions.ObjectPool;
怎么自己建一個最小 demo 跑起來?
先建一個控制臺項目:
dotnet new console -n ObjectPoolDemo cd ObjectPoolDemo dotnet add package Microsoft.Extensions.ObjectPool
然后把 Program.cs 改成下面這樣:
using Microsoft.Extensions.ObjectPool;
using System.Text;
var provider = new DefaultObjectPoolProvider();
var pool = provider.CreateStringBuilderPool();
var sb = pool.Get();
try
{
sb.Append("Hello ");
sb.Append("ObjectPool");
Console.WriteLine(sb.ToString());
}
finally
{
pool.Return(sb);
}
最后執(zhí)行:
dotnet run
如果正常輸出:
Hello ObjectPool
那就說明這條最小鏈路已經(jīng)通了。
這個最小 demo 里最重要的不是 API 名字,而是這個使用姿勢:
- 先
Get() - 用完以后一定
Return() - 最穩(wěn)妥的寫法是放在
try/finally
ObjectPool的核心工作方式是什么?
不用先看源碼,先抓住主線就夠了。
你可以把它粗略理解成這樣:
Get:
池里有空閑對象 -> 直接拿
池里沒有 -> 新建一個Return:
對象可復(fù)用 + 池里還有空間 -> 放回去
否則 -> 直接丟棄
這意味著一個很容易被忽略的事實:
池的大小限制的是“最多保留多少個可復(fù)用對象”,不是“程序一共最多只能創(chuàng)建多少個對象”。
如果并發(fā)一高,池子里不夠用,ObjectPool 仍然會創(chuàng)建新對象。
所以它不是限流器,也不是容量控制器。
DefaultObjectPoolProvider、ObjectPool<T>、PooledObjectPolicy<T>分別是什么?
這幾個類型建議一起理解。
1.ObjectPool<T>
這是抽象池本身。
它定義的就是:
Get()Return()
2.DefaultObjectPoolProvider
這是默認(rèn)的池工廠。
你通常不會每次都直接 new 某個內(nèi)部池實現(xiàn),而是用它來創(chuàng)建池:
var provider = new DefaultObjectPoolProvider(); var pool = provider.CreateStringBuilderPool();
或者:
var provider = new DefaultObjectPoolProvider(); var pool = provider.Create(new MyBufferPolicy());
3.PooledObjectPolicy<T>
這個很關(guān)鍵。
它決定了兩件事:
- 對象怎么創(chuàng)建
- 對象歸還時能不能重新進(jìn)入池
也就是說,池化不僅是“有個容器”,還要有一套規(guī)則。
自定義對象池怎么寫?
如果你要池化的是自己的類型,通常會先寫一個策略類。
先準(zhǔn)備一個可復(fù)用對象:
public sealed class ReusableBuffer
{
public byte[] Buffer { get; } = new byte[4096];
public int Length { get; set; }
public void Reset()
{
Length = 0;
Array.Clear(Buffer, 0, Buffer.Length);
}
}
然后寫一個策略:
using Microsoft.Extensions.ObjectPool;
public sealed class ReusableBufferPolicy : PooledObjectPolicy<ReusableBuffer>
{
public override ReusableBuffer Create()
{
return new ReusableBuffer();
}
public override bool Return(ReusableBuffer obj)
{
obj.Reset();
return true;
}
}
最后創(chuàng)建并使用池:
var provider = new DefaultObjectPoolProvider();
var pool = provider.Create(new ReusableBufferPolicy());
var buffer = pool.Get();
try
{
buffer.Buffer[0] = 100;
buffer.Length = 1;
}
finally
{
pool.Return(buffer);
}
這里最重要的是 Return() 里的邏輯。
因為對象要不要重新進(jìn)入池,不是無條件的。
你完全可以在這里做判斷:
- 對象狀態(tài)不對,不回池
- 對象太大,不回池
- 對象被污染,不回池
例如:
public override bool Return(ReusableBuffer obj)
{
if (obj.Buffer.Length > 1024 * 1024)
{
return false;
}
obj.Reset();
return true;
}
這類寫法在工程上很有價值,因為有些對象一旦膨脹得太大,繼續(xù)留在池里反而會浪費內(nèi)存。
IResettable是什么?
官方實現(xiàn)還提供了一個很實用的思路:
IResettable
如果對象本身就知道怎么把自己恢復(fù)到可復(fù)用狀態(tài),那它就更適合池化。
可以把它理解成:
PooledObjectPolicy<T>負(fù)責(zé)池規(guī)則IResettable更像對象自己聲明“我知道怎么重置自己”
這類設(shè)計的好處是職責(zé)更清楚:
- 池負(fù)責(zé)借還
- 對象負(fù)責(zé)恢復(fù)
在 ASP.NET Core 里怎么接到 DI?
如果你不想手動在每個地方 new DefaultObjectPoolProvider,更自然的方式通常是接進(jìn) DI。
例如:
using Microsoft.Extensions.ObjectPool;
var builder = WebApplication.CreateBuilder(args);
builder.Services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.TryAddSingleton<ObjectPool<StringBuilder>>(sp =>
{
var provider = sp.GetRequiredService<ObjectPoolProvider>();
return provider.CreateStringBuilderPool();
});
后面在服務(wù)里直接注入:
public sealed class MessageBuilderService
{
private readonly ObjectPool<StringBuilder> _pool;
public MessageBuilderService(ObjectPool<StringBuilder> pool)
{
_pool = pool;
}
public string Build(string name)
{
var sb = _pool.Get();
try
{
sb.Append("hello ");
sb.Append(name);
return sb.ToString();
}
finally
{
_pool.Return(sb);
}
}
}
這種寫法更適合真實項目,因為池本身通常應(yīng)該是:
- 單例
- 長生命周期
- 全局復(fù)用
從源碼心智模型看,它內(nèi)部大致是什么樣?
不用先盯實現(xiàn)細(xì)節(jié),先記住一個更重要的事實:
ObjectPool 追求的是“盡量低成本地復(fù)用少量對象”,不是“做一個嚴(yán)格、復(fù)雜、功能很全的資源管理器”。
所以它的實現(xiàn)思路也很務(wù)實:
- 盡量快速取到對象
- 盡量快速還回對象
- 放不下就算了
也就是說,它不是那種:
- 排隊非常嚴(yán)格
- 絕不超量創(chuàng)建
- 生命周期管理很復(fù)雜
的重型池模型。
這也是為什么它在熱點路徑里更好用:
- 邏輯簡單
- 開銷可控
- 行為可預(yù)測
它適合什么場景?
優(yōu)先在這些場景里考慮它:
- 高頻創(chuàng)建、短生命周期對象
- 對象可以明確重置
- 熱點路徑對分配和
GC比較敏感 - 對象復(fù)用收益明顯高于管理成本
典型例子包括:
StringBuilder- 文本拼接緩沖對象
- 臨時解析器
- 可重復(fù)使用的請求上下文輔助對象
- 某些序列化 / 反序列化輔助對象
它不適合什么場景?
這部分比“怎么用”更重要,因為對象池很容易被濫用。
1. 對象本身很輕,直接new成本極低
比如一個只有幾個字段的小對象,直接池化很可能得不償失。
因為你引入了:
- 借還成本
- 重置成本
- 代碼復(fù)雜度
2. 對象狀態(tài)復(fù)雜,很難可靠重置
如果對象里帶著很多內(nèi)部狀態(tài)、外部引用、事件、句柄、線程上下文,那池化風(fēng)險會明顯上升。
這時候最大的風(fēng)險不是“沒優(yōu)化到”,而是:
- 臟狀態(tài)泄漏到下一次使用
3. 對象會長時間被占用
對象池最適合的是:
- 短借短還
如果對象拿走以后要用很久,那池化收益會越來越低。
4. 想拿它當(dāng)緩存
對象池不是為了讓對象一直留著給未來業(yè)務(wù)命中,它只是復(fù)用實例。
5. 想靠它解決并發(fā)限制
它不會因為池大小是 32,就保證系統(tǒng)同時最多只存在 32 個對象。
不夠用的時候,它還是會繼續(xù)創(chuàng)建。
使用時最容易踩的坑
1. 忘記歸還
最直接,也最常見。
所以建議形成固定寫法:
var item = pool.Get();
try
{
// use item
}
finally
{
pool.Return(item);
}
2. 歸還前沒重置干凈
這會直接導(dǎo)致臟數(shù)據(jù)串到下一次調(diào)用。
3. 歸還后繼續(xù)使用對象
這是很危險的一類 bug。
一旦對象已經(jīng)回池,它理論上隨時可能被別人再次借走。
4. 池化大對象,但不控制膨脹
有些對象會隨著業(yè)務(wù)輸入越長越大。
這時如果不做策略控制,池里可能慢慢堆滿“已經(jīng)膨脹過的大對象”,反而拉高常駐內(nèi)存。
一個比較務(wù)實的判斷標(biāo)準(zhǔn)
要不要上 ObjectPool,一般先看四件事:
- 這個對象是不是熱點路徑里高頻創(chuàng)建?
- 創(chuàng)建和回收成本是不是已經(jīng)值得關(guān)注?
- 它能不能被可靠重置?
- 池化帶來的復(fù)雜度,值不值得這點收益?
如果這四個問題里,有兩個以上答不上來,那通常先別急著池化。
總結(jié)
ObjectPool 最值得理解的,不是 API 有多簡單,而是它背后的取舍:
- 用少量額外管理成本
- 換熱點路徑上的更少分配和更低
GC壓力
所以它真正適合的是:
- 高頻
- 短生命周期
- 可重置
- 復(fù)用收益明顯
一句話收尾:
ObjectPool 不是“讓對象永遠(yuǎn)不創(chuàng)建”,而是“讓適合復(fù)用的對象別老是重復(fù)創(chuàng)建”。
到此這篇關(guān)于C#.NET ObjectPool 對象復(fù)用、池化策略與使用邊界的文章就介紹到這了,更多相關(guān)C# ObjectPool 對象復(fù)用和池化策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
unity實現(xiàn)鼠標(biāo)跟隨(ITween)
這篇文章主要為大家詳細(xì)介紹了unity實現(xiàn)鼠標(biāo)跟隨,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-04-04
C#編程實現(xiàn)向并口設(shè)備發(fā)送指令、獲取并口設(shè)備的狀態(tài)
這篇文章主要介紹了C#編程實現(xiàn)向并口設(shè)備發(fā)送指令、獲取并口設(shè)備的狀態(tài),本文直接給出實例代碼,需要的朋友可以參考下2015-06-06

