IDictionaryから派生し、プライベートSyncRootオブジェクトを定義することで、C#でスレッドセーフなディクショナリを実装できました。
public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
private readonly object syncRoot = new object();
private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();
public object SyncRoot
{
get { return syncRoot; }
}
public void Add(TKey key, TValue value)
{
lock (syncRoot)
{
d.Add(key, value);
}
}
// more IDictionary members...
}
次に、コンシューマ全体でこのSyncRootオブジェクトをロックします(複数のスレッド):
例:
lock (m_MySharedDictionary.SyncRoot)
{
m_MySharedDictionary.Add(...);
}
私はそれを機能させることができましたが、これはsomeいコードをもたらしました。私の質問は、スレッドセーフな辞書を実装するより良い、よりエレガントな方法はありますか?
Peterが言ったように、クラス内にすべてのスレッドセーフをカプセル化できます。公開または追加するイベントに注意し、ロックの外側で呼び出されるようにする必要があります。
public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
private readonly object syncRoot = new object();
private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();
public void Add(TKey key, TValue value)
{
lock (syncRoot)
{
d.Add(key, value);
}
OnItemAdded(EventArgs.Empty);
}
public event EventHandler ItemAdded;
protected virtual void OnItemAdded(EventArgs e)
{
EventHandler handler = ItemAdded;
if (handler != null)
handler(this, e);
}
// more IDictionary members...
}
編集: MSDNドキュメントは、列挙は本質的にスレッドセーフではないことを指摘しています。これが、同期オブジェクトをクラスの外部に公開する1つの理由になります。アプローチする別の方法は、すべてのメンバーに対してアクションを実行し、メンバーの列挙をロックする方法を提供することです。これに伴う問題は、その関数に渡されたアクションが辞書のメンバーを呼び出すかどうかがわからないことです(デッドロックが発生します)。同期オブジェクトを公開することで、コンシューマはこれらの決定を下すことができ、クラス内のデッドロックを隠しません。
同時実行性をサポートする.NET 4.0クラスの名前は ConcurrentDictionary
です。
内部的に同期しようとすると、抽象化のレベルが低すぎるため、ほぼ間違いなく不十分です。次のようにAdd
およびContainsKey
操作を個別にスレッドセーフにするとします。
public void Add(TKey key, TValue value)
{
lock (this.syncRoot)
{
this.innerDictionary.Add(key, value);
}
}
public bool ContainsKey(TKey key)
{
lock (this.syncRoot)
{
return this.innerDictionary.ContainsKey(key);
}
}
次に、このスレッドセーフと思われるコードを複数のスレッドから呼び出すとどうなりますか?常に問題なく動作しますか?
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
簡単な答えはノーです。ある時点で、Add
メソッドは、キーが辞書に既に存在することを示す例外をスローします。スレッドセーフな辞書を使用すると、どのようになりますか?別のスレッドがContainsKey
とAdd
の呼び出しの間に変更する可能性があるため、各操作がスレッドセーフであるという理由だけで、2つの操作の組み合わせはそうではありません。
つまり、このタイプのシナリオを正しく記述するには、ロックが必要です外部辞書、例えば.
lock (mySafeDictionary)
{
if (!mySafeDictionary.ContainsKey(someKey))
{
mySafeDictionary.Add(someKey, someValue);
}
}
しかし、外部ロックコードを記述する必要があるため、内部同期と外部同期を混在させているため、不明瞭なコードやデッドロックなどの問題が常に発生します。したがって、最終的には次のいずれかを実行する方がよいでしょう。
通常のDictionary<TKey, TValue>
および外部で同期し、複合操作を囲みます。または
別のインターフェイスで新しいスレッドセーフラッパーを作成します(つまり、IDictionary<T>
)AddIfNotContained
メソッドなどの操作を結合するため、そこからの操作を結合する必要はありません。
(私は自分で#1に行く傾向があります)
プロパティを介してプライベートロックオブジェクトを公開しないでください。ロックオブジェクトは、ランデブーポイントとして機能することのみを目的として、プライベートに存在する必要があります。
標準ロックを使用してパフォーマンスが低いことが判明した場合、Wintellectの Power Threading ロックのコレクションは非常に便利です。
説明している実装方法にはいくつかの問題があります。
個人的には、スレッドセーフクラスを実装する最良の方法は不変性を経由することです。これにより、スレッドセーフで発生する可能性のある問題の数が本当に減ります。詳細については Eric Lippertのブログ をご覧ください。
コンシューマオブジェクトのSyncRootプロパティをロックする必要はありません。辞書のメソッド内にあるロックで十分です。
詳細に:最終的に起こるのは、辞書が必要以上に長い時間ロックされることです。
あなたのケースで起こることは次のとおりです。
スレッドAがSyncRootのロックを取得するとしますbefore m_mySharedDictionary.Addの呼び出し。次に、スレッドBはロックの取得を試みますが、ブロックされます。実際、他のすべてのスレッドはブロックされます。スレッドAは、Addメソッドを呼び出すことができます。 Addメソッド内のロックステートメントで、スレッドAは既にロックを所有しているため、ロックを再度取得できます。メソッド内およびメソッド外でロックコンテキストを終了すると、スレッドAはすべてのロックを解放し、他のスレッドが続行できるようにします。
SharedDictionaryクラスのAddメソッド内のロックステートメントは同じ効果を持つため、単にコンシューマーがAddメソッドを呼び出すことを許可できます。この時点で、冗長ロックがあります。連続して発生することが保証される必要があるディクショナリオブジェクトに対して2つの操作を実行する必要がある場合にのみ、ディクショナリメソッドの1つ以外のSyncRootをロックします。
辞書を作り直してみませんか?読み取りが多数の書き込みである場合、ロックはすべての要求を同期します。
例
private static readonly object Lock = new object();
private static Dictionary<string, string> _dict = new Dictionary<string, string>();
private string Fetch(string key)
{
lock (Lock)
{
string returnValue;
if (_dict.TryGetValue(key, out returnValue))
return returnValue;
returnValue = "find the new value";
_dict = new Dictionary<string, string>(_dict) { { key, returnValue } };
return returnValue;
}
}
public string GetValue(key)
{
string returnValue;
return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
}