原則上所有的參考型別物件都可以通過物件池來提供,但是在具體的應用中需要權衡是否值得用,雖然物件池能夠通過物件復用的方式避免GC,但是它存盤的物件會耗用記憶體,如果物件復用的頻率很小,使用物件池是不值的,如果某個小物件的使用周期很短,能夠確保GC在第0代就能將其回收,這樣的物件其實也不太適合放在物件池中,因為第0代GC的性能其實是很高的,除此之外,物件釋放到物件池之后就有可能被其他執行緒提取出來,如果釋放的時機不對,有可能造成多個執行緒同時操作同一個物件,總之,我們在使用之前得考慮當前場景是否適用物件池,在使用的時候嚴格按照“有借有還”、“不用才還”的原則,
目錄
一、池化集合
二、池化StringBuilder
三、ArrayPool<T>
四、MemoryPool<T>
一、池化集合
我們知道一個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); } } }
如上面的代碼片段所示,我們直接可以呼叫ObjectPoolProvider的CreateStringBuilderPool擴展方法就可以得到針對StringBuilder的物件池物件(型別為ObjectPool<StringBuilder>),我們上面演示實體一樣,我們指定的也是StringBuilder物件的初始和最大容量,池化StringBuilder物件的核心體現在對應的策略型別上,即如下這個StringBuilderPooledObjectPolicy型別,
public class StringBuilderPooledObjectPolicy : PooledObjectPolicy<StringBuilder> { public int InitialCapacity { get; set; } = 100; public int MaximumRetainedCapacity { get; set; } = 4 * 1024; public override StringBuilder Create()=> new StringBuilder(InitialCapacity); public override bool Return(StringBuilder obj) { if (obj.Capacity > MaximumRetainedCapacity) { return false; } obj.Clear(); return true; } }
可以看出它的定義和我們前面定義的FoobarListPolicy型別如出一轍,在默認情況下,池化StringBuilder物件的初始化和最大容量分別為100和5096,如下所示的是ObjectPoolProvider用于創建ObjectPool<StringBuilder>物件的兩個CreateStringBuilderPool擴展方法的定義,
public static class ObjectPoolProviderExtensions { public static ObjectPool<StringBuilder> CreateStringBuilderPool( this ObjectPoolProvider provider) => provider.Create(new StringBuilderPooledObjectPolicy()); public static ObjectPool<StringBuilder> CreateStringBuilderPool( this ObjectPoolProvider provider, int initialCapacity, int maximumRetainedCapacity) { var policy = new StringBuilderPooledObjectPolicy() { InitialCapacity = initialCapacity, MaximumRetainedCapacity = maximumRetainedCapacity, }; return provider.Create(policy); } }
三、ArrayPool<T>
接下來介紹的和前面的內容沒有什么關系,但同屬于我們常用物件池使用場景,我們在編程的時候會大量使用到集合,集合型別(像基于鏈表的集合除外)很多都采用一個陣列作為內部存盤,所以會有前面所說的擴容問題,如果這個陣列很大,還會造成GC的壓力,我們在前面已經采用池化集合的方案解決了這個問題,其實這個問題還有另外一種解決方案,
在很多情況下,當我們需要創建一個物件的時候,實際上需要的一段確定長度的連續物件序列,假設我們將陣列物件進行池化,當我們需要一段定長的物件序列的時候,從池中提取一個長度大于所需長度的可用陣列,并從中截取可用的連續片段(一般從頭開始)就可以了,在使用完之后,我們無需執行任何的釋放操作,直接將陣列物件歸還到物件池中就可以了,這種基于陣列的物件池使用方式可以利用ArrayPool<T>來實作,
public abstract class ArrayPool<T> { public abstract T[] Rent(int minimumLength); public abstract void Return(T[] array, bool clearArray); public static ArrayPool<T> Create(); public static ArrayPool<T> Create(int maxArrayLength, int maxArraysPerBucket); public static ArrayPool<T> Shared { get; } }
如上面的代碼片段所示,抽象型別ArrayPool<T>同樣提供了完成物件池兩個基本操作的方法,其中Rent方法從物件池中“借出”一個不小于(不是等于)指定長度的陣列,該陣列最終通過Return方法釋放到物件池,Return方法的clearArray引數表示在歸還陣列之前是否要將其清空,這取決我們針對陣列的使用方式,如果我們每次都需要覆寫原始的內容,就沒有必要額外執行這種多余操作,
我們可以通過靜態方法Create創建一個ArrayPool<T>物件,池化的陣列并未直接存盤在物件池中,長度接近的多個陣列會被封裝成一個桶(Bucket)中,這樣的好處是在執行Rent方法的時候可以根據指定的長度快速找到最為匹配的陣列(大于并接近指定的長度),物件池存盤的是一組Bucket物件,允許的陣列長度越大,桶的數量越多,Create方法除了可以指定陣列允許最大長度,還可以指定每個桶的容量,除了呼叫靜態Create方法創建一個獨占使用的ArrayPool<T>物件之外,我們可以使用靜態屬性Shared回傳一個應用范圍內共享的ArrayPool<T>物件,ArrayPool<T>的使用非常方便,如下的代碼片段演示了一個讀取檔案的實體,
class Program { static async Task Main() { using var fs = new FileStream("test.txt", FileMode.Open); var length = (int)fs.Length; var bytes = ArrayPool<byte>.Shared.Rent(length); try { await fs.ReadAsync(bytes, 0, length); Console.WriteLine(Encoding.Default.GetString(bytes, 0, length)); } finally { ArrayPool<byte>.Shared.Return(bytes); } } }
四、MemoryPool<T>
陣列是對托管堆中用于存盤同類物件的一段連續記憶體的表達,而另一個型別Memory<T>則具有更加廣泛的應用,因為它不僅僅可以表示一段連續的托管(Managed)記憶體,還可以表示一段連續的Native記憶體,甚至執行緒堆疊記憶體,具有如下定義的MemoryPool<T>表示針對Memory<T>型別的物件池,
public abstract class MemoryPool<T> : IDisposable { public abstract int MaxBufferSize { get; } public static MemoryPool<T> Shared { get; } public void Dispose(); protected abstract void Dispose(bool disposing); public abstract IMemoryOwner<T> Rent(int minBufferSize = -1); } public interface IMemoryOwner<T> : IDisposable { Memory<T> Memory { get; } }
MemoryPool<T>和ArrayPool<T>具有類似的定義,比如通過靜態屬性Shared獲取當前應用全域共享的MemoryPool<T>物件,通過Rent方法從物件池中借出一個不小于指定大小的Memory<T>物件,不同的是,MemoryPool<T>的Rent方法并沒有直接回傳一個Memory<T>物件,而是一個封裝了該物件的IMemoryOwner<T>物件,MemoryPool<T>也沒有定義一個用來釋放Memory<T>物件的Reurn方法,這個操作是通過IMemoryOwner<T>物件的Dispose方法完成的,如果采用MemoryPool<T>,前面針對ArrayPool<T>的演示實體可以改寫成如下的形式,
class Program { static async Task Main() { using var fs = new FileStream("test.txt", FileMode.Open); var length = (int)fs.Length; using (var memoryOwner = MemoryPool<byte>.Shared.Rent(length)) { await fs.ReadAsync(memoryOwner.Memory); Console.WriteLine(Encoding.Default.GetString( memoryOwner.Memory.Span.Slice(0,length))); } } }
物件池在 .NET (Core)中的應用[1]: 編程篇
物件池在 .NET (Core)中的應用[2]: 設計篇
物件池在 .NET (Core)中的應用[3]: 擴展篇
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/295797.html
標籤:.NET技术
