.NET Core中如何實現或使用對象池?
前言
池這個概念大傢都很熟悉,比如我們經常聽到數據庫連接池和線程池。它是一種基於使用預先分配資源集合的性能優化思想。
簡單說,對象池就是對象的容器,旨在優化資源的使用,通過在一個容器中池化對象,並根據需要重復使用這些池化對象來滿足性能上的需求。當一個對象被激活時,便被從池中取出。當對象被停用時,它又被放回池中,等待下一個請求。對象池一般用於對象的初始化過程代價較大或使用頻率較高的場景。
那在 .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 提供的對象池功能還是挺靈活的。普通場景使用使用默認的池化策略、默認的對象池和默認的對象池提供者就可以滿足需求,也可以自定義其中任意某部件來實現比較特殊或復雜的需求。
對象池的使用原則是:有借有還,再借不難。當調用組件從對象池中借走一個對象實例,使用完後應立即歸還給對象池,以便重復利用,避免因過多的對象初始化影響系統性能。
到此這篇關於.NET Core中如何實現或使用對象池的文章就介紹到這瞭,更多相關.NET Core使用對象池內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- .NET Core對象池的應用:設計篇
- 詳解從ObjectPool到CAS指令
- Asp.net core程序中使用微軟的依賴註入框架
- .NET Core對象池的應用:擴展篇
- 詳解.Net緩存之MemoryCahe