對象池在 .NET (Core)中的應用[1]: 編程篇


借助於有效的自動化垃圾回收機制,.NET讓開發人員不在關心對象的生命周期,但實際上很多性能問題都來源於GC。並不說.NET的GC有什么問題,而是對象生命周期的跟蹤和管理本身是需要成本的,不論交給應用還是框架來做,都會對性能造成影響。在一些對性能比較敏感的應用中,我們可以通過對象復用的方式避免垃圾對象的產生,進而避免GC因對象回收導致的性能損失。對象池是對象復用的一種常用的方式。.NET提供了一個簡單高效的對象池框架,並使用在ASP.NET自身框架中。這個對象池狂框架由“Microsoft.Extensions.ObjectPool”這個NuGet包提供,我們可以通過添加這個NuGet包它引入我們的應用中。接下來我們就通過一些簡單的示例來演示一下對象池的基本編程模式。

目錄
一、對象的借與還
二、依賴注入
三、池化對象策略
四、對象池的大小
五、對象的釋放

一、對象的借與還

和絕大部分的對象池編程方式一樣,當我們需要消費某個對象的時候,我們不會直接創建它,而是選擇從對象池中“借出”一個對象。一般來說,如果對象池為空,或者現有的對象都正在被使用,它會自動幫助我們完成對象的創建。借出的對象不再使用的時候,我們需要及時將其“歸還”到對象池中以供后續復用。我們在使用.NET的對象池框架時,主要會使用如下這個ObjectPool<T>類型,針對池化對象的借與還體現在它的GetReturn方法中。

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方法三次。演示實例運行之后會在控制台上輸出如下所示的結果,可以看出每輪迭代使用的三個對象都是一樣的。每次迭代,它們從對象池中被借出,使用完之后又回到池中供下一次迭代使用。

image

二、依賴注入

我們知道依賴注入是已經成為 .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。

image

如果對象池的大小為當前機器處理器數量的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個對象,所以每次迭代都必須額外創建一個新的對象。

image

五、對象的釋放

由於對象池容納的對象數量是有限的,如果現有的所有對象已經被提取出來,它會提供一個新創建的對象。從另一方面講,我們從對象池得到的對象在不需要的時候總是會還回去,但是對象池可能容不下那么多對象,它只能將其丟棄,被丟棄的對象將最終被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方法會被調用,我們定義其中的釋放操作得以被及時執行。

image

對象池在 .NET (Core)中的應用[1]: 編程篇
對象池在 .NET (Core)中的應用[2]: 設計篇
對象池在 .NET (Core)中的應用[3]: 擴展篇


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM