《程式設計篇》已經涉及到了物件池模型的大部分核心介面和型別。物件池模型其實是很簡單的,不過其中有一些為了提升效能而刻意為之的實現細節倒是值得我們關注。總的來說,物件池模型由三個核心物件構成,它們分別是表示物件池的ObjectPool<T>物件、物件值提供者的ObjectPoolProvider物件,已及控制池化物件建立與釋放行為的IPooledObjectPolicy<T>物件,我們先來介紹最後一個物件。
目錄
一、 IPooledObjectPolicy<T>
二、ObjectPool<T>
DefaultObjectPool<T>
DisposableObjectPool<T>
三、ObjectPoolProvider
一、 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)中的應用[1]: 程式設計篇
物件池在 .NET (Core)中的應用[2]: 設計篇
物件池在 .NET (Core)中的應用[3]: 擴充套件篇