借助于有效的自動化垃圾回識訓制,.NET讓開發人員不在關心物件的生命周期,但實際上很多性能問題都來源于GC,并不說.NET的GC有什么問題,而是物件生命周期的跟蹤和管理本身是需要成本的,不論交給應用還是框架來做,都會對性能造成影響,在一些對性能比較敏感的應用中,我們可以通過物件復用的方式避免垃圾物件的產生,進而避免GC因物件回收導致的性能損失,物件池是物件復用的一種常用的方式,.NET提供了一個簡單高效的物件池框架,并使用在ASP.NET自身框架中,這個物件池狂框架由“Microsoft.Extensions.ObjectPool”這個NuGet包提供,我們可以通過添加這個NuGet包它引入我們的應用中,接下來我們就通過一些簡單的示例來演示一下物件池的基本編程模式,
目錄
一、物件的借與還
二、依賴注入
三、池化物件策略
四、物件池的大小
五、物件的釋放
一、物件的借與還
和絕大部分的物件池編程方式一樣,當我們需要消費某個物件的時候,我們不會直接創建它,而是選擇從物件池中“借出”一個物件,一般來說,如果物件池為空,或者現有的物件都正在被使用,它會自動幫助我們完成物件的創建,借出的物件不再使用的時候,我們需要及時將其“歸還”到物件池中以供后續復用,我們在使用.NET的物件池框架時,主要會使用如下這個ObjectPool<T>型別,針對池化物件的借與還體現在它的Get和Return方法中,
public abstract class ObjectPool<T> where T: class { public abstract T Get(); public abstract void Return(T obj); }
我們接下來利用一個簡單的控制臺程式來演示物件池的基本編程模式,在添加了針對“Microsoft.Extensions.ObjectPool”這個NuGet包的參考之后,我們定義了如下這個FoobarService型別來表示希望池化復用的服務物件,如代碼片段所示,FoobarService具有一個自增整數表示Id屬性作為每個實體的唯一標識,靜態欄位_latestId標識當前分發的最后一個標識,
public class FoobarService { internal static int _latestId; public int Id { get; } public FoobarService() => Id = Interlocked.Increment(ref _latestId); }
通過物件池的方式來使用FoobarService物件體現在如下的代碼片段中,我們通過呼叫ObjectPool型別的靜態方法Create<FoobarService>方法得到針對FoobarService型別的物件池,這是一個ObjectPool<FoobarService>物件,針對單個FoobarService物件的使用體現在本地方法ExecuteAsync中,如代碼片段所示,我們呼叫ObjectPool<FoobarService>物件的Get方法從物件池中借出一個Foobar物件,為了確定物件是否真的被復用,我們在控制臺上列印出物件的標識,我們通過延遲1秒鐘模擬針對服務物件的長時間使用,并在最后通過呼叫ObjectPool<FoobarService>物件的Return方法將借出的物件釋放到物件池中,
class Program { static async Task Main() { var objectPool = ObjectPool.Create<FoobarService>(); while (true) { Console.Write("Used services: "); await Task.WhenAll(Enumerable.Range(1, 3).Select(_ => ExecuteAsync())); Console.Write("\n"); } async Task ExecuteAsync() { var service = objectPool.Get(); try { Console.Write($"{service.Id}; "); await Task.Delay(1000); } finally { objectPool.Return(service); } } } }
在Main方法中,我們構建了一個無限回圈,并在每次迭代中并行執行ExecuteAsync方法三次,演示實體運行之后會在控制臺上輸出如下所示的結果,可以看出每輪迭代使用的三個物件都是一樣的,每次迭代,它們從物件池中被借出,使用完之后又回到池中供下一次迭代使用,

二、依賴注入
我們知道依賴注入是已經成為 .NET Core的基本編程模式,針對物件池的編程最好也采用這樣的編程方式,如果采用依賴注入,容器提供的并不是代表物件池的ObjectPool<T>物件,而是一個ObjectPoolProvider物件,顧名思義, ObjectPoolProvider物件作為物件池的提供者,用來提供針對指定物件型別的ObjectPool<T>物件,
.NET提供的大部分框架都提供了針對IServiceCollection介面的擴展方法來注冊相應的服務,但是物件池框架并沒有定義這樣的擴展方法,所以我們需要采用原始的方式來完成針對ObjectPoolProvider的注冊,如下面的代碼片段所示,在創建出ServiceCollection物件之后,我們通過呼叫AddSingleton擴展方法注冊了ObjectPoolProvider的默認實作型別DefaultObjectPoolProvider,
class Program { static async Task Main() { var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .Create<FoobarService>(); … } }
在利用ServiceCollection物件創建出代表依賴注入容器的IServiceProvider物件之后,我們利用它提取出ObjectPoolProvider物件,并通過呼叫其Create<T>方法得到表示物件池的ObjectPool<FoobarService>物件,改動的程式執行之后同樣會在控制臺輸出如上圖所示的結果,
三、池化物件策略
通過前面的實體演示可以看出,物件池在默認情況下會幫助我們完成物件的創建作業,我們可以想得到,它會在物件池無可用物件的時候會呼叫默認的建構式來創建提供的物件,如果池化物件型別沒有默認的建構式呢?或者我們希望執行一些初始化操作呢?
在另一方面,當不在使用的物件被歸還到物件池之前,很有可能會執行一些釋放性質的操作(比如集合物件在歸還之前應該被清空),還有一種可能是物件有可能不能再次復用(比如它內部維護了一個處于錯誤狀態并無法恢復的網路連接),那么它就不能被釋放會物件池,上述的這些需求都可以通過IPooledObjectPolicy<T>介面表示的池化物件策略來解決,
同樣以我們演示實體中使用的FoobarService型別,如果并不希望用戶直接呼叫建構式來創建對應的實體,所以我們按照如下的方式將其建構式改為私有,并定義了一個靜態的工廠方法Create來創建FoobarService物件,當FoobarService型別失去了默認的無參建構式之后,我們演示的程式將無法編譯,
public class FoobarService { internal static int _latestId; public int Id { get; } private FoobarService() => Id = Interlocked.Increment(ref _latestId); public static FoobarService Create() => new FoobarService(); }
為了解決這個問題,我們為FoobarService型別定義一個代表池化物件策略的FoobarPolicy型別,如代碼片段所示,FoobarPolicy型別實作了IPooledObjectPolicy<FoobarService>介面,實作的Create方法通過呼叫FoobarSerivice型別的靜態同名方法完成針對物件的創建,另一個方法Return可以用來執行一些物件歸還前的釋放操作,它的回傳值表示該物件還能否回到池中供后續使用,由于FoobarService物件可以被無限次復用,所以實作的Return方法直接回傳True,
public class FoobarPolicy : IPooledObjectPolicy<FoobarService> { public FoobarService Create() => FoobarService.Create(); public bool Return(FoobarService obj) => true; }
在呼叫ObjectPoolProvider物件的Create<T>方法針對指定的型別創建對應的物件池的時候,我們將一個IPooledObjectPolicy<T>物件作為引數,創建的物件池將會根據該物件定義的策略來創建和釋放物件,
class Program { static async Task Main() { var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .Create(new FoobarPolicy()); … } }
四、物件池的大小
物件池容納物件的數量總歸是有限的,默認情況下它的大小為當前機器處理器數量的2倍,這一點可以通過一個簡單的實體來驗證一下,如下面的代碼片段所示,我們將演示程式中每次迭代并發執行ExecuteAsync方法的數量設定為當前機器處理器數量的2倍,并將最后一次創建的FoobarService物件的ID列印出來,為了避免控制臺上的無效輸出,我們將ExecuteAsync方法中的控制臺輸出代碼移除,
class Program { static async Task Main() { var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .Create(new FoobarPolicy()); var poolSize = Environment.ProcessorCount * 2; while (true) { while (true) { await Task.WhenAll(Enumerable.Range(1, poolSize).Select(_ => ExecuteAsync())); Console.WriteLine($"Last service: {FoobarService._latestId}"); } } async Task ExecuteAsync() { var service = objectPool.Get(); try { await Task.Delay(1000); } finally { objectPool.Return(service); } } } }
上面這個演示實體表達的意思是:物件池的大小和物件消費率剛好是一致的,在這種情況下,消費的每一個物件都是從物件池中提取出來,并且能夠成功還回去,那么物件的創建數量就是物件池的大小,下圖所示的是演示程式運行之后再控制臺上的輸出結果,整個應用的生命周期范圍內一共只會有16個物件被創建出來,因為我當前機器的處理器數量為8,

如果物件池的大小為當前機器處理器數量的2倍,那么我們倘若將物件的消費率提高,意味著池化的物件將無法滿足消費需求,新的物件將持續被創建出來,為了驗證我們的想法,我們按照如下的方式將每次迭代執行任務的數量加1,
class Program { static async Task Main() { var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .Create(new FoobarPolicy()); var poolSize = Environment.ProcessorCount * 2; while (true) { while (true) { await Task.WhenAll(Enumerable.Range(1, poolSize + 1) .Select(_ => ExecuteAsync())); Console.WriteLine($"Last service: {FoobarService._latestId}"); } } … } }
再次運行改動后的程式,我們會在控制臺上看到如下圖所示的輸出結果,由于每次迭代針對物件的需求量是17,但是物件池只能提供16個物件,所以每次迭代都必須額外創建一個新的物件,

五、物件的釋放
由于物件池容納的物件數量是有限的,如果現有的所有物件已經被提取出來,它會提供一個新創建的物件,從另一方面講,我們從物件池得到的物件在不需要的時候總是會還回去,但是物件池可能容不下那么多物件,它只能將其丟棄,被丟棄的物件將最終被GC回收,如果物件型別實作了IDisposable介面,在它不能回到物件池的情況下,它的Dispose方法應該被立即執行,
為了驗證不能正常回歸物件池的物件能否被及時釋放,我們再次對演示的程式作相應的修改,我們讓FoobarService型別實作IDisposable介面,并在實作的Dispose方法中將自身ID輸出到控制臺上,然后我們按照如下的方式以每次迭代并發量高于物件池大小的方式消費物件,
class Program { static async Task Main() { var objectPool = new ServiceCollection().AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>() .BuildServiceProvider() .GetRequiredService<ObjectPoolProvider>() .Create(new FoobarPolicy()); while (true) { Console.Write("Disposed services:"); await Task.WhenAll(Enumerable.Range(1, Environment.ProcessorCount * 2 + 3).Select(_ => ExecuteAsync())); Console.Write("\n"); } async Task ExecuteAsync() { var service = objectPool.Get(); try { await Task.Delay(1000); } finally { objectPool.Return(service); } } } } public class FoobarService: IDisposable { internal static int _latestId; public int Id { get; } private FoobarService() => Id = Interlocked.Increment(ref _latestId); public static FoobarService Create() => new FoobarService(); public void Dispose() => Console.Write($"{Id}; "); }
演示程式運行之后會在控制臺上輸出如下圖所示的結果,可以看出對于每次迭代消費的19個物件,只有16個能夠正常回歸物件池,有三個將被丟棄并最終被GC回收,由于這樣的物件將不能被復用,它的Dispose方法會被呼叫,我們定義其中的釋放操作得以被及時執行,

物件池在 .NET (Core)中的應用[1]: 編程篇
物件池在 .NET (Core)中的應用[2]: 設計篇
物件池在 .NET (Core)中的應用[3]: 擴展篇
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/295781.html
標籤:.NET Core
