スレッドセーフな辞書を実装する最良の方法は何ですか?


109

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

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


3
それについて醜いと思うのは何ですか?
Asaf R

1
彼は、SharedDictionaryクラスのコンシューマー内のコード全体で持っているすべてのロックステートメントを参照していると思います。SharedDictionaryオブジェクトのメソッドにアクセスするたびに、呼び出しコードをロックしています。
Peter Meyer、

Addメソッドを使用する代わりに、値ex-m_MySharedDictionary ["key1"] = "item1"を割り当てて実行してみてください。これはスレッドセーフです。
testuser

回答:


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


@fryguybob:同期オブジェクトを公開した唯一の理由は、列挙型でした。慣例として、コレクションを列挙しているときにのみ、そのオブジェクトに対してロックを実行します。
GP。

1
辞書が大きすぎない場合は、コピーを列挙してクラスに組み込むことができます。
fryguybob 2008年

2
私の辞書は大きすぎず、それでうまくいったと思います。私がしたことは、プライベートディクショナリのコピーを持つディクショナリの新しいインスタンスを返すCopyForEnum()という新しいパブリックメソッドを作成することでした。その後、このメソッドは列挙のために呼び出され、SyncRootは削除されました。ありがとう!
GP。

12
また、ディクショナリ操作は粒度が細かい傾向があるため、これは本質的にスレッドセーフなクラスではありません。if(dict.Contains(whatever)){dict.Remove(whatever);に沿った小さなロジック。dict.Add(whatever、newval); }は、確実に発生するのを待っている競合状態です。
台座、

207

同時実行性をサポートする.NET 4.0クラスの名前はConcurrentDictionaryです。


4
独自の.NETは解決策を持っている場合、応答としてこれをマークしてください、あなたはカスタムdictoniaryを必要としない
アルベルト・レオン

27
(他の回答は.NET 4.0(2010年にリリースされた)が存在するずっと前に書かれたことを思い出してください。)
Jeppe Stig Nielsen

1
残念ながら、これはロックフリーのソリューションではないため、SQL Server CLRセーフアセンブリでは役に立たない。ここで説明されているようなものが必要になるでしょう:cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdfまたはおそらくこの実装:github.com/hackcraft/Ariadne
Triynko

2
本当に古いですが、ConcurrentDictionaryとディクショナリを使用すると、パフォーマンスが大幅に低下する可能性があることに注意してください。これは、高価なコンテキスト切り替えの結果である可能性が高いため、使用する前にスレッドセーフな辞書が必要であることを確認してください。
2016年

60

抽象化のレベルが低すぎるため、内部で同期しようとすることはほぼ確実に不十分です。次のように、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メソッドは、キーが辞書にすでに存在することを示す例外をスローします。スレッドセーフな辞書を使用すると、どうすればよいでしょうか。各操作は、スレッドセーフであるまあという理由だけで、二つの動作の組み合わせは、別のスレッドがあなたへの呼び出しの間で、それを修正する可能性があるので、ないContainsKeyAdd

つまり、このタイプのシナリオを正しく書くには、辞書のにロックが必要です。たとえば、

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

しかし、今、外部ロックコードを記述しなければならないため、内部同期と外部同期を混同しているため、常にコードが不明確でデッドロックなどの問題が発生します。したがって、最終的には次のいずれかをお勧めします。

  1. 通常Dictionary<TKey, TValue>を使用して外部で同期し、それに複合操作を含めます、または

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

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


10
.NET 4.0には、コレクションやディクショナリなどのスレッドセーフコンテナーが多数含まれていることを指摘しておく必要があります。これらには、標準コレクションとは異なるインターフェイスがあります(つまり、上記のオプション2を実行しています)。
グレッグビーチ、

1
また、下層のクラスが破棄されたときにそれを知ることができる適切な列挙子(ディクショナリを書き込みたいメソッド)を設計する適切な列挙子を設計する場合、粗雑なロックでさえ提供される細分性は、シングルライターのマルチリーダーアプローチにとって十分な場合が多いことにも注意する必要があります。未処理の列挙子が存在する場合は、辞書をコピーで置き換える必要があります)。
スーパーキャット2012

6

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

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


5

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

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

個人的には、スレッドセーフクラスを実装する最良の方法は、不変性による方法です。スレッドセーフで発生する可能性のある問題の数を本当に減らします。チェックアウトエリックリペットのブログを詳細については。


3

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

詳しく説明するには: 何が起こっているのかというと、辞書が必要以上に長い時間ロックされているということです。

あなたのケースで何が起こるかは次のとおりです:

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

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


1
真実ではありません...内部でスレッドセーフな次の2つの操作を実行しても、コードブロック全体がスレッドセーフであるとは限りません。例えば:if(!myDict.ContainsKey(someKey)){myDict.Add(someKey、someValue); それがContainsKeyとAddがスレッドセーフであっても、スレッドセーフではありません
Tim

あなたの主張は正しいですが、私の答えと質問の文脈から外れています。質問を見ると、ContainsKeyの呼び出しについても、私の回答についても触れていません。私の答えは、元の質問の例に示されているSyncRootのロックを取得することです。lockステートメントのコンテキスト内で、1つ以上のスレッドセーフな操作が実際に安全に実行されます。
Peter Meyer

彼がやっていることはすべてディクショナリに追加することだと思いますが、 "//より多くのIDictionaryメンバー..."があるため、ある時点で、彼はディクショナリからデータを読み戻したいと思うこともあります。その場合は、外部からアクセスできるロック機構が必要です。ディクショナリ自体のSyncRootであるか、ロックのみに使用される別のオブジェクトであるかは関係ありませんが、そのようなスキームがないと、コード全体はスレッドセーフではありません。
Tim

外部ロックメカニズムは、質問の彼の例で示したとおりです:lock(m_MySharedDictionary.SyncRoot){m_MySharedDictionary.Add(...); }-以下を実行することは完全に安全です:lock(m_MySharedDictionary.SyncRoot){if(!つまり、外部ロックメカニズムは、パブリックプロパティSyncRootを操作するロックステートメントです。
Peter Meyer

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.