私は多くの優れたオブジェクト プールの実装を見てきました。例: C# オブジェクト プーリング パターンの実装。
しかし、スレッドセーフなものは常にロックを使用し、Interlocked.* 操作を使用しようとしないようです。
オブジェクトをプールに返すことを許可しないものを作成するのは簡単に思えます (Interlocked.Increments というポインタを持つ大きな配列だけです)。しかし、オブジェクトを返す方法を書く方法が思い浮かびません。誰かがこれをしましたか?
私は多くの優れたオブジェクト プールの実装を見てきました。例: C# オブジェクト プーリング パターンの実装。
しかし、スレッドセーフなものは常にロックを使用し、Interlocked.* 操作を使用しようとしないようです。
オブジェクトをプールに返すことを許可しないものを作成するのは簡単に思えます (Interlocked.Increments というポインタを持つ大きな配列だけです)。しかし、オブジェクトを返す方法を書く方法が思い浮かびません。誰かがこれをしましたか?
片方向リンクリストとして構築されたロックフリーキューでそれを行いました。以下には無関係なものがいくつか切り取られており、その部分を削除してテストしていませんが、少なくともアイデアを提供する必要があります.
internal sealed class LockFreeQueue<T>
{
private sealed class Node
{
public readonly T Item;
public Node Next;
public Node(T item)
{
Item = item;
}
}
private volatile Node _head;
private volatile Node _tail;
public LockFreeQueue()
{
_head = _tail = new Node(default(T));
}
#pragma warning disable 420 // volatile semantics not lost as only by-ref calls are interlocked
public void Enqueue(T item)
{
Node newNode = new Node(item);
for(;;)
{
Node curTail = _tail;
if (Interlocked.CompareExchange(ref curTail.Next, newNode, null) == null) //append to the tail if it is indeed the tail.
{
Interlocked.CompareExchange(ref _tail, newNode, curTail); //CAS in case we were assisted by an obstructed thread.
return;
}
else
{
Interlocked.CompareExchange(ref _tail, curTail.Next, curTail); //assist obstructing thread.
}
}
}
public bool TryDequeue(out T item)
{
for(;;)
{
Node curHead = _head;
Node curTail = _tail;
Node curHeadNext = curHead.Next;
if (curHead == curTail)
{
if (curHeadNext == null)
{
item = default(T);
return false;
}
else
Interlocked.CompareExchange(ref _tail, curHeadNext, curTail); // assist obstructing thread
}
else
{
item = curHeadNext.Item;
if (Interlocked.CompareExchange(ref _head, curHeadNext, curHead) == curHead)
{
return true;
}
}
}
}
#pragma warning restore 420
}
プーリングの理由が割り当てとコレクションの生のパフォーマンスの考慮事項である場合、これが割り当てと収集を行うという事実は、それをかなり役に立たなくします。基礎となるリソースの取得および/または解放に費用がかかるため、またはインスタンスが使用中に「学習した」情報をキャッシュするためである場合、それが適している可能性があります。
オブジェクト プールが必要な理由をよく考えてください。ここでは、プールされるオブジェクトについては説明しません。ほとんどのオブジェクトでは、マネージ ヒープを使用すると必要な機能が提供され、独自のコードで新しいプール マネージャーを作成するという面倒な作業は必要ありません。オブジェクトが確立しにくい、または解放しにくいリソースをカプセル化する場合にのみ、マネージ コードでのオブジェクト プールを検討する価値があります。
自分で行う必要がある場合は、プール アクセスの最適化に役立つ軽量のリーダー/ライター ロックがあります。
http://theburningmonk.com/2010/02/threading-using-readerwriterlockslim/
.Net 4 の Concurrent コレクションを見たことがありますか。
参照オブジェクトを返す際の問題は、最初にアクセスをロックしようとする試み全体が無効になることです。基本的な lock() コマンドを使用して、オブジェクトのスコープ外のリソースへのアクセスを制御することはできません。つまり、従来の getter/setter 設計は機能しません。
機能する可能性があるのは、ロック可能なリソースを含むオブジェクトであり、リソースを利用するラムダまたはデリゲートを渡すことができます。オブジェクトはリソースをロックし、デリゲートを実行し、デリゲートが完了するとロックを解除します。これは基本的に、コードの実行をロックしているオブジェクトの手に委ねますが、Interlocked よりも複雑な操作が可能になります。
別の可能な方法は、ゲッターとセッターを公開することですが、「チェックアウト」モデルを使用して独自のアクセス制御を実装します。スレッドが値を「取得」できる場合、現在のスレッドへの参照をロックされた内部リソースに保持します。そのスレッドがセッターを呼び出したり、アボートしたりするまで、ゲッターにアクセスしようとする他のすべてのスレッドは Yield ループに保持されます。リソースが再度チェックインされると、次のスレッドがそれを取得できます。
public class Library
{
private Book controlledBook
private Thread checkoutThread;
public Book CheckOutTheBook()
{
while(Thread.CurrentThread != checkoutThread && checkoutThread.IsAlive)
thread.CurrentThread.Yield();
lock(this)
{
checkoutThread = Thread.CurrentThread;
return controlledBook;
}
}
public void CheckInTheBook(Book theBook)
{
if(Thread.CurrentThread != checkoutThread)
throw new InvalidOperationException("This thread does not have the resource checked out.");
lock(this)
{
checkoutThread = null;
controlledBook = theBook;
}
}
}
ここで、オブジェクトのユーザー間の協力が必要であることに注意してください。特に、このロジックはセッターに関してはかなり単純です。本をチェックアウトせずにチェックインすることは不可能です。この規則は消費者には明らかでない場合があり、不適切に使用すると未処理の例外が発生する可能性があります。また、基本的な C# の知識があれば、参照型を取得した場合に行った変更がすべての場所に反映されることはわかっていても、終了する前にオブジェクトの使用をやめる場合は、すべてのユーザーがオブジェクトをチェックインすることを知っておく必要があります。ただし、これは、スレッドセーフでないリソースへの基本的な「一度に 1 つずつ」アクセス制御として使用できます。
良い質問。ハイ パフォーマンス ソフトウェアを作成する場合、高速オブジェクト プールを使用してゼロ割り当てパターンを採用することが重要です。
Microsoft は Apache License 2.0 の下でオブジェクト プールをリリースしました
ロックの使用を回避し、Interlocked.CompareExchange のみを割り当て (Get) に使用します。ほとんどのユースケースである一度に少数のオブジェクトを取得して解放すると、特に高速に見えます。オブジェクトの大量のバッチを取得してからバッチを解放すると、最適化されていないように見えるため、アプリケーションがそのように動作する場合は変更する必要があります。
あなたが示唆したように、 Interlocked.Increment アプローチはより一般的であり、バッチのユースケースにより適していると思います。
http://sourceroslyn.io/#Microsoft.CodeAnalysis.Workspaces/ObjectPool%25601.cs,98aa6d9b3c4e313b
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will
// make everything about 2-3x slower
//
// #define TRACE_LEAKS
// define DETECT_LEAKS to detect possible leaks
// #if DEBUG
// #define DETECT_LEAKS //for now always enable DETECT_LEAKS in debug.
// #endif
using System;
using System.Diagnostics;
using System.Threading;
#if DETECT_LEAKS
using System.Runtime.CompilerServices;
#endif
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>
internal class ObjectPool<T> where T : class
{
[DebuggerDisplay("{Value,nq}")]
private struct Element
{
internal T Value;
}
/// <remarks>
/// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
/// which does not have that type (since it compiles against .NET 2.0).
/// </remarks>
internal 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;
#if DETECT_LEAKS
private static readonly ConditionalWeakTable<T, LeakTracker> leakTrackers = new ConditionalWeakTable<T, LeakTracker>();
private class LeakTracker : IDisposable
{
private volatile bool disposed;
#if TRACE_LEAKS
internal volatile object Trace = null;
#endif
public void Dispose()
{
disposed = true;
GC.SuppressFinalize(this);
}
private string GetTrace()
{
#if TRACE_LEAKS
return Trace == null ? "" : Trace.ToString();
#else
return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n";
#endif
}
~LeakTracker()
{
if (!this.disposed && !Environment.HasShutdownStarted)
{
var trace = GetTrace();
// If you are seeing this message it means that object has been allocated from the pool
// and has not been returned back. This is not critical, but turns pool into rather
// inefficient kind of "new".
Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END");
}
}
}
#endif
internal ObjectPool(Factory factory)
: this(factory, Environment.ProcessorCount * 2)
{ }
internal ObjectPool(Factory factory, int size)
{
Debug.Assert(size >= 1);
_factory = factory;
_items = new Element[size - 1];
}
private T CreateInstance()
{
var 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>
internal 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();
}
#if DETECT_LEAKS
var tracker = new LeakTracker();
leakTrackers.Add(inst, tracker);
#if TRACE_LEAKS
var frame = CaptureStackTrace();
tracker.Trace = frame;
#endif
#endif
return inst;
}
private T AllocateSlow()
{
var 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>
internal void Free(T obj)
{
Validate(obj);
ForgetTrackedObject(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)
{
var 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;
}
}
}
/// <summary>
/// Removes an object from leak tracking.
///
/// This is called when an object is returned to the pool. It may also be explicitly
/// called if an object allocated from the pool is intentionally not being returned
/// to the pool. This can be of use with pooled arrays if the consumer wants to
/// return a larger array to the pool than was originally allocated.
/// </summary>
[Conditional("DEBUG")]
internal void ForgetTrackedObject(T old, T replacement = null)
{
#if DETECT_LEAKS
LeakTracker tracker;
if (leakTrackers.TryGetValue(old, out tracker))
{
tracker.Dispose();
leakTrackers.Remove(old);
}
else
{
var trace = CaptureStackTrace();
Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END");
}
if (replacement != null)
{
tracker = new LeakTracker();
leakTrackers.Add(replacement, tracker);
}
#endif
}
#if DETECT_LEAKS
private static Lazy<Type> _stackTraceType = new Lazy<Type>(() => Type.GetType("System.Diagnostics.StackTrace"));
private static object CaptureStackTrace()
{
return Activator.CreateInstance(_stackTraceType.Value);
}
#endif
[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?");
}
}
}
}
安全でない方法で使用する必要があることも、Interlocked を使用することに実際の利点があるとは思えません。Lock は、オブジェクトのメモリ空間でビット フラグを変更するだけです。非常に高速です。インターロックは、メモリではなくレジスタで実行できるため、少し優れています。
パフォーマンスの問題が発生していますか? そのようなコードの主な目的は何ですか? 結局のところ、C# は、ユーザーがビジネス上の問題に集中できるように、メモリ管理を抽象化するように設計されています。
自分でメモリを管理し、安全でないポインタを使用する必要がある場合は、メモリ領域を固定する必要がある = 余分なパフォーマンス コストがかかることに注意してください。