110

IDictionary から派生させ、プライベート SyncRoot オブジェクトを定義することで、C# でスレッド セーフな Dictionary を実装することができました。

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(...);
}

私はそれを機能させることができましたが、これは醜いコードになりました。私の質問は、スレッドセーフな辞書を実装するためのより優れた、よりエレガントな方法はありますか?

4

8 に答える 8

207

並行性をサポートする.NET4.0クラスの名前はConcurrentDictionary

于 2010-09-13T19:11:21.937 に答える
63

内部で同期しようとすると、抽象化のレベルが低すぎるため、ほぼ確実に不十分になります。次のようにAddandContainsKey操作を個別にスレッドセーフにするとします。

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メソッドはキーが辞書に既に存在することを示す例外をスローします。スレッドセーフなディクショナリでこれがどのように行われるのでしょうか? 各操作がスレッドセーフであるという理由だけで、2 つの操作の組み合わせはそうではありません。別のスレッドが と の呼び出しの間にそれを変更する可能性があるからContainsKeyですAdd

つまり、このタイプのシナリオを正しく記述するには、辞書の外側にロックが必要です。

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

しかし現在、外部ロック コードを書かなければならないため、内部同期と外部同期が混同されており、不明確なコードやデッドロックなどの問題が常に発生しています。したがって、最終的には、おそらく次のいずれかを実行することをお勧めします。

  1. 法線Dictionary<TKey, TValue>を使用して外部で同期し、その複合操作を囲む、または

  2. IDictionary<T>メソッドなどの操作を結合する別のインターフェイス (つまり、 ではない) を使用して、新しいスレッドセーフ ラッパーを作成し、AddIfNotContainedメソッドからの操作を結合する必要がないようにします。

(私は自分で#1に行く傾向があります)

于 2008-12-30T00:07:59.377 に答える
43

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 つになる可能性があります。これにアプローチする別の方法は、すべてのメンバーに対してアクションを実行し、メンバーの列挙をロックするためのメソッドを提供することです。これの問題は、その関数に渡されたアクションが辞書のメンバーを呼び出すかどうかがわからないことです (デッドロックが発生します)。同期オブジェクトを公開すると、消費者はこれらの決定を下すことができ、クラス内のデッドロックを隠すことはありません。

于 2008-10-01T14:49:48.007 に答える
6

プロパティを介してプライベート ロック オブジェクトを公開しないでください。ロック オブジェクトは、ランデブー ポイントとして機能するためだけに非公開で存在する必要があります。

標準ロックを使用してパフォーマンスが低下することが判明した場合は、Wintellect のPower Threadingコレクションのロックが非常に役立ちます。

于 2008-10-01T14:51:53.757 に答える
5

あなたが説明している実装方法にはいくつかの問題があります。

  1. 同期オブジェクトを公開しないでください。そうすることで、消費者がオブジェクトをつかんでロックをかけることができるようになり、乾杯します。
  2. スレッドセーフクラスを使用して非スレッドセーフインターフェイスを実装しています。私見これはあなたに将来の費用がかかります

個人的には、スレッドセーフクラスを実装する最良の方法は不変性を使用することです。これにより、スレッドセーフで発生する可能性のある問題の数が大幅に減少します。詳細については、 EricLippertのブログをご覧ください。

于 2008-10-01T15:13:32.597 に答える
3

コンシューマー オブジェクトで SyncRoot プロパティをロックする必要はありません。辞書のメソッド内にあるロックで十分です。

詳しく説明する: 最終 的には、辞書が必要以上に長期間ロックされることになります。

あなたの場合は次のようになります。

m_mySharedDictionary.Add を呼び出す前に、スレッド A が SyncRoot のロックを取得するとします。次に、スレッド B がロックを取得しようとしますが、ブロックされます。実際、他のすべてのスレッドはブロックされています。スレッド A は Add メソッドを呼び出すことができます。Add メソッド内の lock ステートメントで、スレッド A は既にロックを所有しているため、再度ロックを取得できます。メソッド内でロック コンテキストを終了し、次にメソッド外で、スレッド A はすべてのロックを解放し、他のスレッドが続行できるようにします。

SharedDictionary クラスの Add メソッド内の lock ステートメントが同じ効果を持つため、任意のコンシューマーが Add メソッドを呼び出せるようにするだけです。この時点で、冗長ロックが設定されています。ディクショナリ オブジェクトに対して、連続して発生することが保証される必要がある 2 つの操作を実行する必要がある場合は、いずれかのディクショナリ メソッドの外部でのみ SyncRoot をロックします。

于 2008-10-01T14:42:32.220 に答える
0

辞書を再作成してみませんか?読み取りが多数の書き込みである場合、ロックはすべての要求を同期します。

    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);
    }
于 2012-11-16T15:27:07.323 に答える
-6

コレクションと同期

于 2008-10-01T14:52:50.587 に答える