C#中透過ObjectPool重用物件提高程式效能

VinciYan發表於2024-12-10

環境說明:

  • .NET 8.0
  • Microsoft.Extensions.DependencyInjection v9.0.0
  • Microsoft.Extensions.ObjectPool v9.0.0

ObjectPool重用物件

先看微軟官方文件的描述:

Microsoft.Extensions.ObjectPool它支援將一組物件保留在記憶體中以供重用,而不是允許對物件進行垃圾回收

如果要管理的物件具有以下特徵,應用可能希望使用物件池:

  • 分配/初始化成本高昂
  • 表示有限資源
  • 可預見地頻繁使用

比如使用物件池來重用StringBuilder例項。 StringBuilder分配並管理自己的緩衝區來儲存字元資料。如果經常使用StringBuilder來實現功能,重用這些物件會帶來效能優勢

物件池並不總是能提高效能:

  • 除非物件的初始化成本很高,否則從池中獲取物件通常較慢
  • 在池解除分配之前,池管理的物件無法解除分配
  • 僅在使用應用或庫的真實場景收集效能資料後才使用物件池

注意:ObjectPool不限制分配的物件數量,但限制保留的物件數量

使用ObjectPool

呼叫Get獲取物件,呼叫Return返回物件。 不必返回每個物件。 如果某個物件未返回,系統將對其進行垃圾回收

使用示例

這個示例展示瞭如何在控制檯應用程式中使用物件池來重用大型緩衝區,避免重複分配記憶體,提高效能。每次計算雜湊值時都會重用同一個緩衝區物件,而不是建立新的

要點:

  • ObjectPoolProvider新增到依賴項注入 (DI) 容器
  • 實現IResettable介面,以在返回到物件池時自動清除緩衝區的內容

IResettable介面的作用是當物件被返回到物件池時:

  • 物件池會自動呼叫TryReset()方法
  • 可以自定義清除緩衝區中的敏感資料
  • 為下次使用做準備
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using System.Security.Cryptography;

class Program
{
    public class ReusableBuffer : IResettable
    {
        private readonly string _id = Guid.NewGuid().ToString("N").Substring(0, 6);
        private int _useCount = 0;
        public byte[] Data { get; } = new byte[1024 * 1024]; // 1 MB

        public ReusableBuffer()
        {
            Console.WriteLine($"建立新的緩衝區 ID: {_id}");
        }

        public bool TryReset()
        {
            _useCount++;
            Console.WriteLine($"重置緩衝區 ID: {_id}, 使用次數: {_useCount}");
            Array.Clear(Data); // 只重置資料內容
            return true;
        }

        public string GetId() => $"{_id} (使用次數: {_useCount})";
    }

    static void Main(string[] args)
    {
        // 設定依賴注入
        var services = new ServiceCollection();

        // 註冊物件池提供程式
        services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

        // 註冊 ReusableBuffer 的物件池
        services.AddSingleton<ObjectPool<ReusableBuffer>>(serviceProvider =>
        {
            var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
            var policy = new DefaultPooledObjectPolicy<ReusableBuffer>();
            return provider.Create(policy);
        });

        var serviceProvider = services.BuildServiceProvider();

        // 獲取物件池
        var bufferPool = serviceProvider.GetRequiredService<ObjectPool<ReusableBuffer>>();

        // 測試雜湊計算
        Console.WriteLine("輸入字串計算雜湊值 (輸入 'exit' 退出):");

        while (true)
        {
            Console.Write("\n請輸入字串: ");
            string input = Console.ReadLine() ?? "";

            if (input.ToLower() == "exit")
                break;

            var hash = CalculateHash(input, bufferPool);
            Console.WriteLine($"Hash: {hash}");
        }
    }

    static string CalculateHash(string input, ObjectPool<ReusableBuffer> bufferPool)
    {
        var buffer = bufferPool.Get();
        Console.WriteLine($"獲取緩衝區 ID: {buffer.GetId()}");

        try
        {
            // 將輸入字串轉換為位元組並儲存在緩衝區中
            for (var i = 0; i < input.Length; i++)
            {
                buffer.Data[i] = (byte)input[i];
            }

            Span<byte> hash = stackalloc byte[32];
            SHA256.HashData(buffer.Data.AsSpan(0, input.Length), hash);
            return Convert.ToHexString(hash);
        }
        finally
        {
            // 歸還緩衝區到物件池
            Console.WriteLine($"歸還緩衝區 ID: {buffer.GetId()}");
            bufferPool.Return(buffer);
        }
    }
}

程式結果:

輸入字串計算雜湊值 (輸入 'exit' 退出):

請輸入字串: Hello World
建立新的緩衝區 ID: f493ed
獲取緩衝區 ID: f493ed (使用次數: 0)
歸還緩衝區 ID: f493ed (使用次數: 0)
重置緩衝區 ID: f493ed, 使用次數: 1
Hash: A591A6D40BF420404A011733CFB7B190D62C65BF0BCDA32B57B277D9AD9F146E

請輸入字串: Test123
獲取緩衝區 ID: f493ed (使用次數: 1)
歸還緩衝區 ID: f493ed (使用次數: 1)
重置緩衝區 ID: f493ed, 使用次數: 2
Hash: D9B5F58F0B38198293971865A14074F59EBA3E82595BECBE86AE51F1D9F1F65E

請輸入字串: exit

擴充套件類

前面我們透過實現IResettable介面來自動回收重用,如果需要自定義物件建立、複雜初始化或自定義回收重用邏輯的場景,透過策略模式實現,相比透過介面實現,擴充套件性更強

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using System;


namespace Microsoft.Extensions.ObjectPool
{
    internal class PooledObjectByFuncPolicy<T> : PooledObjectPolicy<T> where T : class
    {
        private readonly Func<T> _createfunc;
        private readonly Func<T, bool> _returnfunc;
        private static int _returnCount = 0;
        private static int _createCount = 0;  // 新增建立計數

        public PooledObjectByFuncPolicy(Func<T> createfunc, Func<T, bool> returnfunc)
        {
            _createfunc = createfunc;
            _returnfunc = returnfunc;
        }

        public override T Create()
        {
            int count = Interlocked.Increment(ref _createCount);
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Policy: 建立新物件 (第{count}次建立)");
            return _createfunc.Invoke();
        }

        public override bool Return(T obj)
        {
            int returnCount = Interlocked.Increment(ref _returnCount);
            bool canReuse = _returnfunc.Invoke(obj);
            Console.WriteLine(
                $"[{DateTime.Now:HH:mm:ss}] Policy: 物件返回池中 " +
                $"(第{returnCount}次返回, {(canReuse ? "可以重用" : "不可重用")})");
            return canReuse;
        }
    }
    public static class ObjectPoolExtension
    {
        /// <summary>
        /// 新增型別為<typeparamref name="T"/>至物件池進行復用
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="services"></param>
        /// <returns></returns>
        public static IServiceCollection AddObjectPool<T>(this IServiceCollection services) where T : class,new ()
        {
            return services.AddSingleton(s =>
            {
                var provider = s.GetRequiredService<ObjectPoolProvider>();
                return provider.Create<T>();
            });
        }
        /// <summary>
        /// 將<typeparamref name="T"/>型別新增至物件池, 並使用<paramref name="_create"/>進行建立初始化, 使用<paramref name="_returnfunc"/>歸還
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="services"></param>
        /// <param name="_create"></param>
        /// <param name="_returnfunc"></param>
        /// <returns></returns>
        public static IServiceCollection AddObjectPool<T>(this IServiceCollection services, Func<T> _create, Func<T, bool> _returnfunc) where T : class
        {
            return services.AddSingleton(s =>
            {
                var provider = s.GetRequiredService<ObjectPoolProvider>();
                return provider.Create(new PooledObjectByFuncPolicy<T>(_create, _returnfunc));
            });
        }
        /// <summary>
        ///  將<typeparamref name="T"/>型別新增至物件池, 並使用<paramref name="_create"/>進行建立初始化
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="services"></param>
        /// <param name="_create"></param>
        /// <returns></returns>
        public static IServiceCollection AddObjectPool<T>(this IServiceCollection services, Func<T> _create) where T : class
        {
            return services.AddObjectPool(_create, t => true);
        }
    }
}
  1. PooledObjectByFuncPolicy類

這是一個物件池策略類,負責:

  • 透過傳入的Func委託來建立物件
  • 透過傳入的Func<T, bool>委託來決定物件是否可以被回收重用
  1. ObjectPoolExtension類

提供了三個擴充套件方法,用於在依賴注入容器中註冊物件池:

// 最簡單的註冊方式,適用於有無參建構函式的類
public static IServiceCollection AddObjectPool<T>(this IServiceCollection services) 
    where T : class, new()

// 完整的註冊方式,可以自定義物件的建立和回收邏輯
public static IServiceCollection AddObjectPool<T>(
    this IServiceCollection services, 
    Func<T> _create, 
    Func<T, bool> _returnfunc)

// 簡化版註冊方式,只需要提供建立邏輯
public static IServiceCollection AddObjectPool<T>(
    this IServiceCollection services, 
    Func<T> _create)

應用例項

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using System;
using System.Threading.Tasks;

namespace Microsoft.Extensions.ObjectPool
{
    internal class PooledObjectByFuncPolicy<T> : PooledObjectPolicy<T> where T : class
    {
        private readonly Func<T> _createfunc;
        private readonly Func<T, bool> _returnfunc;
        private static int _returnCount = 0;
        private static int _createCount = 0;  // 新增建立計數

        public PooledObjectByFuncPolicy(Func<T> createfunc, Func<T, bool> returnfunc)
        {
            _createfunc = createfunc;
            _returnfunc = returnfunc;
        }

        public override T Create()
        {
            int count = Interlocked.Increment(ref _createCount);
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Policy: 建立新物件 (第{count}次建立)");
            return _createfunc.Invoke();
        }

        public override bool Return(T obj)
        {
            int returnCount = Interlocked.Increment(ref _returnCount);
            bool canReuse = _returnfunc.Invoke(obj);
            Console.WriteLine(
                $"[{DateTime.Now:HH:mm:ss}] Policy: 物件返回池中 " +
                $"(第{returnCount}次返回, {(canReuse ? "可以重用" : "不可重用")})");
            return canReuse;
        }
    }

    public static class ObjectPoolExtension
    {
        public static IServiceCollection AddObjectPool<T>(this IServiceCollection services) where T : class, new()
        {
            return services.AddSingleton(s =>
            {
                var provider = s.GetRequiredService<ObjectPoolProvider>();
                return provider.Create<T>();
            });
        }

        public static IServiceCollection AddObjectPool<T>(this IServiceCollection services, Func<T> _create, Func<T, bool> _returnfunc) where T : class
        {
            return services.AddSingleton(s =>
            {
                var provider = s.GetRequiredService<ObjectPoolProvider>();
                return provider.Create(new PooledObjectByFuncPolicy<T>(_create, _returnfunc));
            });
        }

        public static IServiceCollection AddObjectPool<T>(this IServiceCollection services, Func<T> _create) where T : class
        {
            return services.AddObjectPool(_create, t => true);
        }
    }
    // 模擬一個耗資源的物件
    public class ExpensiveObject : IDisposable
    {
        public string Id { get; }
        private bool _isDisposed;
        private static int _totalObjects = 0;
        private readonly int _objectNumber;

        public ExpensiveObject()
        {
            _objectNumber = Interlocked.Increment(ref _totalObjects);
            Id = Guid.NewGuid().ToString("N");
            // 模擬耗時的初始化
            Thread.Sleep(5000);
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 建立新物件 #{_objectNumber}: {Id}");
        }

        public void DoWork()
        {
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 物件 #{_objectNumber} ({Id}) 執行工作");
        }

        public void Dispose()
        {
            if (!_isDisposed)
            {
                Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 物件 #{_objectNumber} ({Id}) 被釋放");
                _isDisposed = true;
            }
        }
    }

    // 測試程式
    public class Program
    {
        static async Task Main(string[] args)
        {
            // 1. 配置依賴注入
            var services = new ServiceCollection();
            // 註冊預設物件池提供程式
            services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

            // 使用自定義策略註冊物件池
            services.AddObjectPool<ExpensiveObject>(
                () => new ExpensiveObject(),
                obj =>
                {
                    Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 檢查物件 {obj.Id} 是否可以重用");
                    return true; // 是否允許重用
                }
            );

            using var serviceProvider = services.BuildServiceProvider();

            // 2. 獲取物件池
            var objectPool = serviceProvider.GetRequiredService<ObjectPool<ExpensiveObject>>();

            Console.WriteLine("開始測試物件池...\n");

            for (int i = 0; i < 5; i++)
            {
                await Task.Run(() =>
                {
                    Console.WriteLine($"\n[{DateTime.Now:HH:mm:ss}] 開始第 {i + 1} 次操作");
                    // 從池中獲取物件
                    var obj = objectPool.Get();
                    try
                    {
                        // 使用物件
                        obj.DoWork();
                        Thread.Sleep(100);// 模擬工作時間
                    }
                    finally
                    {
                        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 準備歸還物件到池中");
                        // 歸還物件到池中
                        objectPool.Return(obj);
                    }
                    Console.WriteLine("-------------------");
                });
            }
            Console.WriteLine("\n測試完成!");
        }
    }
}

“是否允許重用”設定為ture時,結果如下:

開始測試物件池...


[11:14:25] 開始第 1 次操作
[11:14:25] Policy: 建立新物件 (第1次建立)
[11:14:30] 建立新物件 #1: d1ef227f13f443c887574b0a826cf954
[11:14:30] 物件 #1 (d1ef227f13f443c887574b0a826cf954) 執行工作
[11:14:30] 準備歸還物件到池中
[11:14:30] 檢查物件 d1ef227f13f443c887574b0a826cf954 是否可以重用
[11:14:30] Policy: 物件返回池中 (第1次返回, 可以重用)
-------------------

[11:14:30] 開始第 2 次操作
[11:14:30] 物件 #1 (d1ef227f13f443c887574b0a826cf954) 執行工作
[11:14:30] 準備歸還物件到池中
[11:14:30] 檢查物件 d1ef227f13f443c887574b0a826cf954 是否可以重用
[11:14:30] Policy: 物件返回池中 (第2次返回, 可以重用)
-------------------

[11:14:30] 開始第 3 次操作
[11:14:30] 物件 #1 (d1ef227f13f443c887574b0a826cf954) 執行工作
[11:14:30] 準備歸還物件到池中
[11:14:30] 檢查物件 d1ef227f13f443c887574b0a826cf954 是否可以重用
[11:14:30] Policy: 物件返回池中 (第3次返回, 可以重用)
-------------------

[11:14:30] 開始第 4 次操作
[11:14:30] 物件 #1 (d1ef227f13f443c887574b0a826cf954) 執行工作
[11:14:30] 準備歸還物件到池中
[11:14:30] 檢查物件 d1ef227f13f443c887574b0a826cf954 是否可以重用
[11:14:30] Policy: 物件返回池中 (第4次返回, 可以重用)
-------------------

[11:14:30] 開始第 5 次操作
[11:14:30] 物件 #1 (d1ef227f13f443c887574b0a826cf954) 執行工作
[11:14:30] 準備歸還物件到池中
[11:14:30] 檢查物件 d1ef227f13f443c887574b0a826cf954 是否可以重用
[11:14:30] Policy: 物件返回池中 (第5次返回, 可以重用)
-------------------

測試完成!
[11:14:30] 物件 #1 (d1ef227f13f443c887574b0a826cf954) 被釋放

透過日誌的時間,我們得知只有第一次建立物件比較耗時

“是否允許重用”設定為false時,結果如下:

開始測試物件池...


[11:16:03] 開始第 1 次操作
[11:16:03] Policy: 建立新物件 (第1次建立)
[11:16:08] 建立新物件 #1: 80923f55ac2445d3a62685e4166ca436
[11:16:08] 物件 #1 (80923f55ac2445d3a62685e4166ca436) 執行工作
[11:16:08] 準備歸還物件到池中
[11:16:08] 檢查物件 80923f55ac2445d3a62685e4166ca436 是否可以重用
[11:16:08] Policy: 物件返回池中 (第1次返回, 不可重用)
[11:16:08] 物件 #1 (80923f55ac2445d3a62685e4166ca436) 被釋放
-------------------

[11:16:08] 開始第 2 次操作
[11:16:08] Policy: 建立新物件 (第2次建立)
[11:16:13] 建立新物件 #2: 5b15f09ff293427a93fe7dce01cf9d3e
[11:16:13] 物件 #2 (5b15f09ff293427a93fe7dce01cf9d3e) 執行工作
[11:16:13] 準備歸還物件到池中
[11:16:13] 檢查物件 5b15f09ff293427a93fe7dce01cf9d3e 是否可以重用
[11:16:13] Policy: 物件返回池中 (第2次返回, 不可重用)
[11:16:13] 物件 #2 (5b15f09ff293427a93fe7dce01cf9d3e) 被釋放
-------------------

[11:16:13] 開始第 3 次操作
[11:16:13] Policy: 建立新物件 (第3次建立)
[11:16:18] 建立新物件 #3: ef65bab6f6a142dbb3a16f7daa668514
[11:16:18] 物件 #3 (ef65bab6f6a142dbb3a16f7daa668514) 執行工作
[11:16:18] 準備歸還物件到池中
[11:16:18] 檢查物件 ef65bab6f6a142dbb3a16f7daa668514 是否可以重用
[11:16:18] Policy: 物件返回池中 (第3次返回, 不可重用)
[11:16:18] 物件 #3 (ef65bab6f6a142dbb3a16f7daa668514) 被釋放
-------------------

[11:16:18] 開始第 4 次操作
[11:16:18] Policy: 建立新物件 (第4次建立)
[11:16:23] 建立新物件 #4: 81b791bb198f4d618ef93a6567618cec
[11:16:23] 物件 #4 (81b791bb198f4d618ef93a6567618cec) 執行工作
[11:16:23] 準備歸還物件到池中
[11:16:23] 檢查物件 81b791bb198f4d618ef93a6567618cec 是否可以重用
[11:16:23] Policy: 物件返回池中 (第4次返回, 不可重用)
[11:16:23] 物件 #4 (81b791bb198f4d618ef93a6567618cec) 被釋放
-------------------

[11:16:23] 開始第 5 次操作
[11:16:23] Policy: 建立新物件 (第5次建立)
[11:16:28] 建立新物件 #5: 60d4737360464aa3bb16d2894df3386c
[11:16:28] 物件 #5 (60d4737360464aa3bb16d2894df3386c) 執行工作
[11:16:28] 準備歸還物件到池中
[11:16:28] 檢查物件 60d4737360464aa3bb16d2894df3386c 是否可以重用
[11:16:28] Policy: 物件返回池中 (第5次返回, 不可重用)
[11:16:28] 物件 #5 (60d4737360464aa3bb16d2894df3386c) 被釋放
-------------------

測試完成!

通知日誌,可以看出,每次都會建立新物件,非常耗時

總結

物件池的核心是物件重用,而物件池的目的是避免頻繁建立和銷燬物件

參考

  • 透過 ASP.NET Core 中的 ObjectPool 重用物件 | Microsoft Learn
  • IoTSharp/IoTSharp.Extensions.DependencyInjection/ObjectPoolExtension.cs at master · IoTSharp/IoTSharp

相關文章