Hệ Thống Quản Lý Đối Tượng Trong .NET Với Object Pool

Hệ Thống Quản Lý Đối Tượng Trong .NET Với Object Pool

Object pool (bản đối tượng) là một cơ chế quản lý bộ nhớ, được thiết kế để tối ưu hóa việc sử dụng tài nguyên hệ thống. Bằng cách lưu trữ và tái sử dụng các đối tượng đã được khởi tạo trước đó, object pool giúp giảm thiểu chi phí khởi tạo đối tượng mới, đặc biệt hữu ích trong các tình huống khởi tạo đối tượng tốn kém tài nguyên hoặc sử dụng thường xuyên.

Khi một đối tượng được yêu cầu, nó sẽ được lấy từ pool. Khi không còn sử dụng, đối tượng sẽ được trả về pool để chờ lần sử dụng tiếp theo.

Trong framework ASP.NET Core (các phiên bản .NET Framework mới hơn), đã có sẵn một implementation cho object pool thông qua thư viện: Microsoft.Extensions.ObjectPool.

Chiến Lược Và Cách Sử Dụng Cơ Bản

Để sử dụng ObjectPool, chúng ta cần định nghĩa một chiến lược (policy) để chỉ định cách tạo đối tượng mới và cách xử lý đối tượng khi trả về pool.

Chiến lược này được thực hiện bằng cách implement interface IPooledObjectPolicy. Dưới đây là một implementation đơn giản:

public class ChiTietChienLuocDoiTuong<T> : PooledObjectPolicy<T> where T : class, new()
{
    /// <inheritdoc />
    public override T TaoMoi()
    {
        return new T();
    }

    /// <inheritdoc />
    public override bool TraLai(T obj)
    {
        if (obj is IKhoiPhuc khoiPhuc)
        {
            return khoiPhuc.ThuKhoiPhuc();
        }

        return true;
    }
}

Khi pool không có đối tượng sẵn sàng, một đối tượng mới sẽ được tạo và trả về. Khi pool đã có đối tượng, một đối tượng hiện có sẽ được lấy ra. Quá trình này được đảm bảo an toàn thread.

Microsoft.Extensions.ObjectPool cung cấp implementation mặc định: DefaultObjectPool, với hai thao tác chính là Get (lấy đối tượng) và Return (trả đối tượng). Khi tạo pool, chúng ta cần cung cấp một chiến lược IPooledObjectPolicy làm tham số.

Dưới đây là ví dụ sử dụng cơ bản của object pool:

public class XeHoi
{
    public int MaSo { get; set; }
    public DateTime ThoiGianTao { get; set; }
    public XeHoi()
    {
        Thread.Sleep(5000); // Mô phỏng việc tạo đối tượng tốn thời gian
        MaSo = new Random().Next();
        ThoiGianTao = DateTime.Now;
    }
}
var chienLuoc = new ChiTietChienLuocDoiTuong<XeHoi>();
var pool = new DefaultObjectPool<XeHoi>(chienLuoc);
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < 10; i++)
{
    var xe = pool.Get();
    Console.WriteLine(i + " Tạo thành công " + " mã số: " + xe.MaSo + " thời gian: " + stopwatch.ElapsedMilliseconds);
    stopwatch.Restart();
    if (i >= 2) // Ba đầu tiên không trả về pool
    {
        pool.Return(xe);
    }
}

Thành phần sử dụng nên lấy đối tượng từ pool và trả lại ngay khi không còn sử dụng, để đối tượng có thể được tái sử dụng, tránh việc tạo mới đối tượng liên tục gây lãng phí tài nguyên.

Ngoài ra, chúng ta có thể chỉ định kích thước tối đa của pool khi khởi tạo DefaultObjectPool.

Tóm Tắt

Nguyên tắc sử dụng object pool là: "Có mượn có trả, mượn lần sau không khó". Khi thành phần sử dụng lấy đối tượng từ pool, sau khi sử dụng xong nên trả lại pool để tái sử dụng, tránh việc khởi tạo quá nhiều đối tượng ảnh hưởng đến hiệu suất hệ thống.

Object pool chủ yếu được sử dụng trong các kịch bản khởi tạo đối tượng tốn thời gian và sử dụng thường xuyên, ví dụ như khi khởi tạo cần đọc tài nguyên mạng. Đôi khi các đối tượng này có tính thời hạn nhất định, không thể sử dụng singleton (sẽ giải thích rõ hơn thông qua chiến lược tùy chỉnh). Vì vậy, IOC không thể thay thế hoàn toàn object pool.

Khởi Tạo Sớm và Kiểm Soát Kích Thước Pool

Tương tự như cache, để tối ưu hiệu suất, chúng ta có thể khởi tạo pool ngay từ đầu khi hệ thống khởi động.

static void ThietLapSoLuongToiDa()
{
    var soLuongPool = 10;
    var chienLuoc = new ChiTietChienLuocDoiTuong<XeHoi>();
    var pool = new DefaultObjectPool<XeHoi>(chienLuoc, soLuongPool); // Pool chứa 10 đối tượng

    List<XeHoi> danhSach = new List<XeHoi>();
    Console.WriteLine("Bắt đầu-----------------------------------");
    for (int i = 0; i < soLuongPool; i++)
    {
        var xe = pool.Get();
        Console.WriteLine("Thêm một đối tượng vào pool");
        danhSach.Add(xe);
    }
    foreach (XeHoi xe in danhSach)
    {
        pool.Return(xe);
    }
    Console.WriteLine("Khởi tạo kết thúc-----------------------------------");
    List<Task> tasks = new List<Task>();
    var soLuongYeuCau = 20;
    for (int i = 0; i < soLuongYeuCau; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            var xe = pool.Get();
            stopwatch.Stop();
            Console.WriteLine("ID thread: " + Thread.CurrentThread.ManagedThreadId + " Tạo thành công, thời gian: " + stopwatch.ElapsedMilliseconds);
        }));
    }
    Task.WaitAll(tasks.ToArray());
}

Kết quả chạy cho thấy: khi số lượng yêu cầu đồng thời vượt quá số lượng đối tượng trong pool, hệ thống sẽ tạo thêm đối tượng mới.

Chiến Lược Pool Tùy Chỉnh

Bằng cách tạo chiến lược pool tùy chỉnh, chúng ta có thể hiểu sâu hơn về cơ chế hoạt động của object pool và phân biệt rõ với các kịch bản sử dụng IOC.


/// <summary>
/// Chiến lược pool tùy chỉnh
/// </summary>
/// <typeparam name="T"></typeparam>
public class ChiTietChienLuocTuChinh : ChiTietChienLuocDoiTuong<XeHoi>
{
    public override XeHoi TaoMoi()
    {
        return base.TaoMoi();
    }
    public override bool TraLai(XeHoi obj)
    {
        if (obj == null) { return false; }
        // Đối tượng hết hạn sau 30 giây
        if (obj.ThoiGianTao.AddSeconds(30) < DateTime.Now)
        {
            return false;
        }
        return base.TraLai(obj);
    }
}
static void ChienLuocTuChinh()
{
    var soLuongPool = 10;
    var chienLuoc = new ChiTietChienLuocTuChinh();
    var pool = new DefaultObjectPool<XeHoi>(chienLuoc, soLuongPool);

    List<XeHoi> danhSach = new List<XeHoi>();
    for (int i = 0; i < soLuongPool; i++)
    {
        var xe = pool.Get();
        Console.WriteLine("Thêm một đối tượng vào pool");
        danhSach.Add(xe);
    }
    // Chờ 40 giây để đối tượng hết hạn khi trả về
    Thread.Sleep(40000);
    foreach (XeHoi xe in danhSach)
    {
        pool.Return(xe);
    }
    Console.WriteLine("Khởi tạo kết thúc-----------------------------------");
    List<Task> tasks = new List<Task>();
    for (int i = 0; i < 5; i++)
    {
        tasks.Add(Task.Run(() =>
        {
            Stopwatch stopwatch = Stopwatch.StartNew();
            var xe = pool.Get();
            stopwatch.Stop();
            Console.WriteLine("ID thread: " + Thread.CurrentThread.ManagedThreadId + " Tạo thành công, thời gian: " + stopwatch.ElapsedMilliseconds);
        }));
    }
    Task.WaitAll(tasks.ToArray());
}

Quan điểm cá nhân: Nếu không cần chiến lược tùy chỉnh cho việc tạo và xử lý đối tượng, thì việc sử dụng singleton trong IOC không khác gì object pool. Tuy nhiên, khi cần tùy chỉnh cách tạo và xử lý đối tượng, object pool là lựa chọn phù hợp.

Thẻ: C# .NET Object Pool Quản lý bộ nhớ tối ưu hiệu suất

Đăng vào ngày 26 tháng 5 lúc 09:24