.NET Core對象池的應用:設計篇

《編程篇》已經涉及到瞭對象池模型的大部分核心接口和類型。對象池模型其實是很簡單的,不過其中有一些為瞭提升性能而刻意為之的實現細節倒是值得我們關註。總的來說,對象池模型由三個核心對象構成,它們分別是表示對象池的ObjectPool<T>對象、對象值提供者的ObjectPoolProvider對象,已及控制池化對象創建與釋放行為的IPooledObjectPolicy<T>對象,我們先來介紹最後一個對象。

一、 IPooledObjectPolicy<T>

我們在《編程篇》已經說過,表示池化對象策略的IPooledObjectPolicy<T>對象不僅僅幫助我們創建對象,還可以幫助我們執行一些對象回歸對象池之前所需的回收操作,對象最終能否回到對象池中也受它的控制。如下面的代碼片段所示,IPooledObjectPolicy<T>接口定義瞭兩個方法,Create方法用來創建池化對象,對象回歸前需要執行的操作體現在Return方法上,該方法的返回值決定瞭指定的對象是否應該回歸對象池。抽象類PooledObjectPolicy<T>實現瞭該接口,我們一般將它作為自定義策略類型的基類。

public interface IPooledObjectPolicy<T>
{
    T Create();
    bool Return(T obj);
}

public abstract class PooledObjectPolicy<T> : IPooledObjectPolicy<T>
{
    protected PooledObjectPolicy(){}

    public abstract T Create();
    public abstract bool Return(T obj);
}

我們默認使用的是如下這個DefaultPooledObjectPolicy<T>類型,由於它直接通過反射來創建池化對象,所以要求泛型參數T必須有一個公共的默認無參構造函數。它的Return方法直接返回True,意味著提供的對象可以被無限制地復用。

public class DefaultPooledObjectPolicy<T> : PooledObjectPolicy<T> where T: class, new()
{
    public override T Create() => Activator.CreateInstance<T>();
    public override bool Return(T obj) => true;
}

二、ObjectPool<T>

對象池通過ObjectPool<T>對象表示。如下面的代碼片段所示,ObjectPool<T>是一個抽象類,池化對象通過Get方法提供給我們,我們在使用完之後調用Return方法將其釋放到對象池中以供後續復用。

public abstract class ObjectPool<T> where T: class
{
    protected ObjectPool(){}

    public abstract T Get();
    public abstract void Return(T obj);
}

DefaultObjectPool<T>

我們默認使用的對象池體現為一個DefaultObjectPool<T>對象,由於針對對象池的絕大部分實現就體現這個類型中,所以它也是本節重點講述的內容。我們在前面一節已經說過,對象池具有固定的大小,並且默認的大小為處理器個數的2倍。我們假設對象池的大小為N,那麼DefaultObjectPool<T>對象會如下圖所示的方式使用一個單一對象和一個長度為N-1的數組來存放由它提供的N個對象。

如下面的代碼片段所示,DefaultObjectPool<T>使用字段_firstItem用來存放第一個池化對象,餘下的則存放在_items字段表示的數組中。值得註意的是,這個數組的元素類型並非池化對象的類型T,而是一個封裝瞭池化對象的結構體ObjectWrapper。如果該數組元素類型改為引用類型T,那麼當我們對某個元素進行復制的時候,運行時會進行類型校驗(要求指定對象類型派生於T),無形之中帶來瞭一定的性能損失(值類型數組就不需求進行派生類型的校驗)。我們在前面提到過,對象池中存在一些性能優化的細節,這就是其中之一。

public class DefaultObjectPool<T> : ObjectPool<T> where T : class
{
    private protected T _firstItem;
    private protected readonly ObjectWrapper[]  _items;
    …
    private protected struct ObjectWrapper
    {
        public T Element;
    }
}

DefaultObjectPool<T>類型定義瞭如下兩個構造函數。我們在創建一個DefaultObjectPool<T>對象的時候會提供一個IPooledObjectPolicy<T>對象並指定對象池的大小。對象池的大小默認設置為處理器數量的2倍體現在第一個構造函數重載中。如果指定的是一個DefaultPooledObjectPolicy<T>對象,表示默認池化對象策略的_isDefaultPolicy字段被設置成True。因為DefaultPooledObjectPolicy<T>對象的Return方法總是返回True,並且沒有任何具體的操作,所以在將對象釋放回對象池的時候就不需要調用Return方法瞭,這是第二個性能優化的細節。

public class DefaultObjectPool<T> : ObjectPool<T> where T : class
{    
    private protected T  _firstItem;
    private protected readonly ObjectWrapper[] _items;
    private protected readonly IPooledObjectPolicy<T> _policy;
    private protected readonly bool  _isDefaultPolicy;
    private protected readonly PooledObjectPolicy<T>  _fastPolicy;
    
    public DefaultObjectPool(IPooledObjectPolicy<T> policy) : this(policy, Environment.ProcessorCount * 2)
    {}

    public DefaultObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
    {
        _policy  = policy ;
        _fastPolicy = policy as PooledObjectPolicy<T>;
        _isDefaultPolicy = IsDefaultPolicy();
        _items  = new ObjectWrapper[maximumRetained - 1];

        bool IsDefaultPolicy()
        {
            var type = policy.GetType();
            return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(DefaultPooledObjectPolicy<>);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private T Create() => _fastPolicy?.Create() ?? _policy.Create();
}

從第二個構造函數的定義可以看出,指定的IPooledObjectPolicy<T>對象除瞭會賦值給_policy字段之外,如果提供的是一個PooledObjectPolicy<T>對象,該對象還會同時賦值給另一個名為_fastPolicy的字段。在進行池化對象的提取和釋放時,_fastPolicy字段表示的池化對象策略會優先選用,這個邏輯體現在Create方法上。因為調用類型的方法比調用接口方法具有更好的性能(所以該字段才會命名為_fastPolicy),這是第三個性能優化的細節。這個細節還告訴我們在自定義池化對象策略的時候,最好將PooledObjectPolicy<T>作為基類,而不是直接實現IPooledObjectPolicy<T>接口。

如下所示的是重寫的Get和Return方法的定義。用於提供池化對象的Get方法很簡單,它會采用原子操作使用Null將_firstItem字段表示的對象“替換”下來,如果該字段不為Null,那麼將其作為返回的對象,反之它會遍歷數組的每個ObjectWrapper對象,並使用Null將其封裝的對象“替換”下來,第一個成功替換下來的對象將作為返回值。如果所有ObjectWrapper對象封裝的對象都為Null,意味著所有對象都被“借出”或者尚未創建,此時返回創建的新對象瞭。

public class DefaultObjectPool<T> : ObjectPool<T> where T : class
{    
    public override T Get()
    {
        var item = _firstItem;
        if (item == null || Interlocked.CompareExchange(ref _firstItem, null, item) != item)
        {
            var items = _items;
            for (var i = 0; i < items.Length; i++)
            {
                item = items[i].Element;
                if (item != null && Interlocked.CompareExchange( ref items[i].Element, null, item) == item)
                {
                    return item;
                }
            }
            item = Create();
        }
        return item;
    }    

    public override void Return(T obj)
    {
        if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
        {
            if (_firstItem != null || Interlocked.CompareExchange(ref _firstItem, obj, null) != null)
            {
                var items = _items;
                for (var i = 0; i < items.Length && Interlocked.CompareExchange( ref items[i].Element, obj, null) != null; ++i)
                {}
            }
        }
    }
    …
}

將對象釋放會對象池的Return方法也很好理解。首先它需要判斷指定的對象能否釋放會對象池中,如果使用的是默認的池化對象策略,答案是肯定的,否則隻能通過調用IPooledObjectPolicy<T>對象的Return方法來判斷。從代碼片段可以看出,這裡依然會優先選擇_fastPolicy字段表示的PooledObjectPolicy<T>對象以獲得更好的性能。

在確定指定的對象可以釋放回對象之後,如果_firstItem字段為Null,Return方法會采用原子操作使用指定的對象將其“替換”下來。如果該字段不為Null或者原子替換失敗,該方法會便利數組的每個ObjectWrapper對象,並采用原子操作將它們封裝的空引用替換成指定的對象。整個方法會在某個原子替換操作成功或者整個便利過程結束之後返回。

DefaultObjectPool<T>之所有使用一個數組附加一個單一對象來存儲池化對象,是因為針對單一字段的讀寫比針對數組元素的讀寫具有更好的性能。從上面給出的代碼可以看出,不論是Get還是Return方法,優先選擇的都是_firstItem字段。如果池化對象的使用率不高,基本上使用的都會是該字段存儲的對象,那麼此時的性能是最高的。

DisposableObjectPool<T>

通過前面的示例演示我們知道,當池化對象類型實現瞭IDisposable接口的情況下,如果某個對象在回歸對象池的時候,對象池已滿,該對象將被丟棄。與此同時,被丟棄對象的Dispose方法將立即被調用。但是這種現象並沒有在DefaultObjectPool<T>類型的代碼中體現出來,這是為什麼呢?實際上DefaultObjectPool<T>還有如下這個名為DisposableObjectPool<T>的派生類。如代碼片段可以看出,表示池化對象類型的泛型參數T要求實現IDisposable接口。如果池化對象類型實現瞭IDisposable接口,通過默認ObjectPoolProvider對象創建的對象池就是一個DisposableObjectPool<T>對象。

internal sealed class DisposableObjectPool<T> : DefaultObjectPool<T>, IDisposable where T : class
{
    private volatile bool _isDisposed;
    public DisposableObjectPool(IPooledObjectPolicy<T> policy) : base(policy)
    {}

    public DisposableObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained) : base(policy, maximumRetained)
    {}

    public override T Get()
    {
        if (_isDisposed)
        {
            throw new ObjectDisposedException(GetType().Name);
        }
        return base.Get();
    }

    public override void Return(T obj)
    {
        if (_isDisposed || !ReturnCore(obj))
        {
            DisposeItem(obj);
        }
    }

    private bool ReturnCore(T obj)
    {
        bool returnedToPool = false;
        if (_isDefaultPolicy || (_fastPolicy?.Return(obj) ?? _policy.Return(obj)))
        {
            if (_firstItem == null && Interlocked.CompareExchange(ref _firstItem, obj, null) == null)
            {
                returnedToPool = true;
            }
            else
            {
                var items = _items;
                for (var i = 0; i < items.Length && !(returnedTooPool = Interlocked.CompareExchange(ref items[i].Element, obj, null) == null); i++)
                {}
            }
        }
        return returnedTooPool;
    }

    public void Dispose()
    {
        _isDisposed = true;
        DisposeItem(_firstItem);
        _firstItem = null;

        ObjectWrapper[] items = _items;
        for (var i = 0; i < items.Length; i++)
        {
            DisposeItem(items[i].Element);
            items[i].Element = null;
        }
    }

    private void DisposeItem(T item)
    {
        if (item is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

從上面代碼片段可以看出,DisposableObjectPool<T>自身類型也實現瞭IDisposable接口,它會在Dispose方法中調用目前對象池中的每個對象的Dispose方法。用於提供池化對象的Get方法除瞭會驗證自身的Disposed狀態之外,並沒有特別之處。當對象未能成功回歸對象池,通過調用該對象的Dispose方法將其釋放的操作體現在重寫的Return方法中。

三、ObjectPoolProvider

表示對象池的ObjectPool<T>對象是通過ObjectPoolProvider提供的。如下面的代碼片段所示,抽象類ObjectPoolProvider定義瞭兩個重載的Create<T>方法,抽象方法需要指定具體的池化對象策略。另一個重載由於采用默認的池化對象策略,所以要求對象類型具有一個默認無參構造函數。

public abstract class ObjectPoolProvider
{
    public ObjectPool<T> Create<T>() where T : class, new() => Create<T>(new DefaultPooledObjectPolicy<T>());
    public abstract ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) where T : class;
}

在前面的示例演示中,我們使用的是如下這個DefaultObjectPoolProvider類型。如代碼片段所示,DefaultObjectPoolProvider派生於抽象類ObjectPoolProvider,在重寫的Create<T>方法中,它會根據泛型參數T是否實現IDisposable接口分別創建DisposableObjectPool<T>和DefaultObjectPool<T>對象。

public class DefaultObjectPoolProvider : ObjectPoolProvider
{
    public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2;
    public override ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) => typeof(IDisposable).IsAssignableFrom(typeof(T))
        ? new DisposableObjectPool<T>(policy, MaximumRetained) : new DefaultObjectPool<T>(policy, MaximumRetained);
}

DefaultObjectPoolProvider類型定義瞭一個標識對象池大小的MaximumRetained屬性,采用處理器數量的兩倍作為默認容量也體現在這裡。這個屬性並非隻讀,所以我們可以利用它根據具體需求調整提供對象池的大小。在ASP.NET應用中,我們基本上都會采用依賴註入的方式利用註入的ObjectPoolProvider對象來創建針對具體類型的對象池。我們在《編程篇》還演示瞭另一種創建對象池的方式,那就是直接調用ObjectPool類型的靜態Create<T>方法,該方法的實現體現在如下所示的代碼片段中。

public static class ObjectPool
{
    public static ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) where T: class, new()
        => new DefaultObjectPoolProvider().Create<T>(policy ?? new DefaultPooledObjectPolicy<T>());
}

到目前為止,我們已經將整個對象池的設計模型進行瞭完整的介紹。總得來說,這是一個簡單、高效並且具有可擴展性的對象池框架,該模型涉及的幾個核心接口和類型體現在如下圖所示的UML中。

.NET Core對象池的應用:編程篇

.NET Core對象池的應用:擴展篇

到此這篇關於.NET Core對象池的應用:設計篇的文章就介紹到這瞭,更多相關.NET Core對象池的應用內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: