C#中物件池ObjectPool的實現
執行緒池是從預先建立的眾多執行緒中 取用 / 歸還 執行緒,通過重複利用執行緒,以節省頻繁新建銷燬執行緒的開銷。
同理,物件池則是為了從預先建立好的眾多相同的物件中 取用 / 歸還 物件,通過重複利用物件,以節省頻繁建立銷燬物件的開銷。
物件池多用於併發多執行緒操作,因此物件池一般要求是執行緒安全的,而且還需要有很高的效能。
這裡摘錄一個在微軟開源的Roslyn編譯器中實現的ObjectPool(刪除了部分除錯程式碼):
using System; using System.Diagnostics; using System.Threading; namespace Microsoft.CodeAnalysis.PooledObjects { /// <summary> /// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose is that /// limited number of frequently used objects can be kept in the pool for further recycling. /// /// Notes: /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there is no space in the /// pool, extra returned objects will be dropped. /// /// 2) it is implied that if object was obtained from a pool, the caller will return it back in a relatively short /// time. Keeping checked out objects for long durations is ok, but reduces usefulness of pooling. Just new up your own. /// /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. Rationale: If /// there is no intent for reusing the object, do not use pool - just use "new". /// </summary> public class ObjectPool<T> where T : class { [DebuggerDisplay("{Value,nq}")] private struct Element { internal T Value; } // Not using System.Func{T} because .NET2.0 does not have that type. public delegate T Factory(); // Storage for the pool objects. The first item is stored in a dedicated field because we expect to be able to // satisfy most requests from it. private T _firstItem; private readonly Element[] _items; // factory is stored for the lifetime of the pool. We will call this only when pool needs to expand. compared to // "new T()", Func gives more flexibility to implementers and faster than "new T()". private readonly Factory _factory; public ObjectPool(Factory factory) : this(factory, Environment.ProcessorCount * 2) { } public ObjectPool(Factory factory, int size) { Debug.Assert(size >= 1); _factory = factory; _items = new Element[size - 1]; } private T CreateInstance() { T inst = _factory(); return inst; } /// <summary> /// Produces an instance. /// </summary> /// <remarks> /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. Note that Free will try /// to store recycled objects close to the start thus statistically reducing how far we will typically search. /// </remarks> public T Allocate() { // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements. Note // that the initial read is optimistically not synchronized. That is intentional. We will interlock only when // we have a candidate. in a worst case we may miss some recently returned objects. Not a big deal. T inst = _firstItem; if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst)) { inst = AllocateSlow(); } return inst; } private T AllocateSlow() { Element[] items = _items; for (int i = 0; i < items.Length; i++) { // Note that the initial read is optimistically not synchronized. That is intentional. We will interlock // only when we have a candidate. in a worst case we may miss some recently returned objects. Not a big deal. T inst = items[i].Value; if (inst != null) { if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst)) { return inst; } } } return CreateInstance(); } /// <summary> /// Returns objects to the pool. /// </summary> /// <remarks> /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. Note that Free will try /// to store recycled objects close to the start thus statistically reducing how far we will typically search in Allocate. /// </remarks> public void Free(T obj) { Validate(obj); if (_firstItem == null) { // Intentionally not using interlocked here. In a worst case scenario two objects may be stored into same // slot. It is very unlikely to happen and will only mean that one of the objects will get collected. _firstItem = obj; } else { FreeSlow(obj); } } private void FreeSlow(T obj) { Element[] items = _items; for (int i = 0; i < items.Length; i++) { if (items[i].Value == null) { // Intentionally not using interlocked here. In a worst case scenario two objects may be stored into // same slot. It is very unlikely to happen and will only mean that one of the objects will get collected. items[i].Value = obj; break; } } } [Conditional("DEBUG")] private void Validate(object obj) { Debug.Assert(obj != null, "freeing null?"); Debug.Assert(_firstItem != obj, "freeing twice?"); var items = _items; for (int i = 0; i < items.Length; i++) { var value = items[i].Value; if (value == null) { return; } Debug.Assert(value != obj, "freeing twice?"); } } } }
原地址連結
http://source.roslyn.codeplex.com/#Microsoft.CodeAnalysis/ObjectPool%25601.cs
這裡面有一個技巧,看微軟的物件池實現就會發現他使用了結構體對泛型型別進行了封裝。
private struct Element
{
internal T Value;
}
private readonly Element[] _items;
詳細解釋參見下面連結的第一,二條技巧,當然裡面也有使用List實現一個物件池
https://www.red-gate.com/simple-talk/dotnet/.net-framework/5-tips-and-techniques-for-avoiding-automatic-gc-collections/
引用連結文章中所講:對於引用型別的陣列,GC除了檢查陣列本身是否存活,還需要遍歷陣列檢查每個元素是否還存活。
而對於結構體型別的陣列,因為結構體不會為空,所以GC只要檢查一下陣列本身是否存活就可以了。
是一個對GC友好的技巧。
另外,Roslyn中實現了多種執行緒池,根據微軟編譯器組的人說法,效能最好的就是上面這個使用結構體對泛型型別進行了封裝的這個ObjectPool<T>.
各種執行緒池的區別參見下文連結:
https://stackoverflow.com/questions/30618067/why-are-there-so-many-implementations-of-object-pooling-in-roslyn