定義
高效能託管陣列緩衝池,可重複使用,用租用空間的方式代替重新分配陣列空間的行為
好處
可以在頻繁建立和銷燬陣列的情況下提高效能,減少垃圾回收器的壓力
使用
- 獲取緩衝池例項:Create/Shared
var pool=ArrayPool[byte].Shared
- 呼叫緩衝池例項Rent()函式,租用緩衝區空間
byte[] array=pool.Rent(1024)
- 呼叫緩衝池例項Return(array[T])函式,歸還租用的空間
pool.Return(array)
Shared
Shared返回為一個靜態共享例項,實際返回了一個TlsOverPerCoreLockedStacksArrayPool
internal sealed class TlsOverPerCoreLockedStacksArrayPool<T> : ArrayPool<T>
{
private static readonly TlsOverPerCoreLockedStacksArrayPool<T> s_shared = new TlsOverPerCoreLockedStacksArrayPool<T>();
public static ArrayPool<T> Shared => s_shared;
}
特點
- 租用陣列長度不可超過 2^20( 1024*1024 = 1 048 576),否則會從GC中重新開闢記憶體空間
- Rent租用陣列實際返回的長度可能比請求的長度大,返回長度一是(16*2^n)
- Return歸還緩衝區的時候,如果不設定clearArray,下一個租用者可能會看到之前的填充的值(在返回的陣列長度剛好是下一個租用者請求的長度時會被看到)
- 緩衝池的記憶體釋放不是實時釋放,在緩衝區空閒時,大概10到20秒之後,會隨著第2代GC一起釋放,分批釋放
- 併發數量持續增長時,緩衝池佔用的記憶體空間也會持續增長,而且似乎沒有上限
耗時對比
private static void TimeMonitor()
{
//隨機生成3000個陣列的長度值
var sizes = new int[30000];
Parallel.For(0, 10000, x => { sizes[x] = new Random().Next(1024 * 800, 1024 * 1024); });
//緩衝池方式租用陣列
var gcAllocate0 = GC.GetTotalAllocatedBytes();
var watch = new Stopwatch();
Console.WriteLine("start");
watch.Start();
for (int i = 0; i < 10000; i++)
{
//CreateArrayByPool(ArrayPool<int>.Shared, 1024 * 1024,sizes[i], false);
var arr = ArrayPool<int>.Shared.Rent(sizes[i]);
for (int j = 0; j < sizes[i]; j++)
{
arr[j] = i;
}
ArrayPool<int>.Shared.Return(arr, true);
}
var time1 = watch.ElapsedMilliseconds;
var gcAllocate1 = GC.GetTotalAllocatedBytes(true);
//new 方式分配陣列空間
watch.Restart();
for (int i = 0; i < 30000; i++)
{
//CreateArrayDefault(i, sizes[i], false);
var arr = new int[sizes[i]];
for (int j = 0; j < sizes[i]; j++)
{
arr[j] = i;
}
}
var time2 = watch.ElapsedMilliseconds;
var gcAllocate2 = GC.GetTotalAllocatedBytes(true);
Console.WriteLine("ArrayPool方式建立陣列耗時:" + time1 + " Gc總分配量" + (gcAllocate1 - gcAllocate0));
Console.WriteLine("預設方式建立陣列耗時:" + time2 + " Gc總分配量" + (gcAllocate2 - gcAllocate1 - gcAllocate0));
}
記憶體使用截圖:左側沒有波動的橫線是緩衝池執行的過程,右側為手動建立陣列的執行過程
執行結果:
ArrayPool方式建立陣列耗時:17545 Gc總分配量4130800
預設方式建立陣列耗時:26870 Gc總分配量37354100896
示例(前端檔案通過後端Api上傳OSS)
private static void PostFileByBytesPool(FormFile file)
{
HttpClient client = new HttpClient() { BaseAddress = new Uri("https://fileserver.com") };
var fileLen = (int)file.Length;
var fileArr = ArrayPool<byte>.Shared.Rent(fileLen);
using var stream = file.OpenReadStream();
stream.Read(fileArr, 0, fileLen);
MultipartFormDataContent content = new MultipartFormDataContent();
content.Add(new ByteArrayContent(fileArr, 0, fileLen), "id_" + Guid.NewGuid().ToString(), file.FileName);
client.PostAsync("/myfile/" + file.FileName, content).Wait();
ArrayPool<byte>.Shared.Return(fileArr, true);
}
Create()
ArrayPool
ConfigurableArrayPool
- maxArrayLength:單次租借的陣列最大長度,不可超過
1024*1024*1024
- maxArraysPerBucket:最多可以存在的未歸還緩衝區數量
通過這兩個引數可以解決Shared方式的兩個問題:
-
自定義單個陣列的最大長度,可以獲取更大的記憶體空間用來儲存大檔案等
-
限定了陣列的長度和最大緩衝區數量,就限定了最大的不可回收記憶體數量,防止高併發時緩衝池記憶體持續增長
示例
//建立一個自定義緩衝池例項,單個陣列最大長度為1024 * 2048,最大可同時租用10個緩衝區
ArrayPool<int> CustomerArrayPool = ArrayPool<int>.Create(1024 * 2048,10);
與Shared不同的是,如果設定CustomerArrayPool=Null
那麼在下一次垃圾回收時該緩衝池所佔的記憶體會立馬全部釋放。
為防止不可預測的風險,應該保持CustomerArrayPool的存活。
同時為了防止記憶體的濫用應該限制CustomerArrayPool的數量