.NET-辞書ロックとConcurrentDictionary


131

ConcurrentDictionary型について十分な情報が見つからなかったので、ここで質問したいと思いました。

現在、私はaを使用しDictionaryて、複数のスレッド(スレッドプールからなので、正確な量のスレッドはありません)によって絶えずアクセスされるすべてのユーザーを保持し、アクセスを同期しています。

私は最近、.NET 4.0に一連のスレッドセーフなコレクションがあることを発見しました。これは非常に楽しいようです。Dictionary同期アクセスを使用する通常のオプションと、ConcurrentDictionaryすでにスレッドセーフなオプションのどちらかを選択できるため、「より効率的で管理しやすい」オプションは何でしょうか。

.NET 4.0への参照 ConcurrentDictionary


回答:


146

スレッドセーフなコレクションと非スレッドセーフなコレクションは、別の方法で見ることができます。

チェックアウト時を除いて、店員のいない店を考えてみましょう。人々が責任ある行動を取らないと、あなたは多くの問題を抱えています。たとえば、店員がピラミッドを構築しているときに、顧客がピラミッド缶から缶を取り出したとしましょう。または、2人の顧客が同じアイテムに同時に到達した場合、誰が勝ちますか?戦いはありますか?これはスレッドセーフではないコレクションです。問題を回避する方法はたくさんありますが、すべて、何らかのロック、または何らかの方法での明示的なアクセスが必要です。

一方、店員が机にいる店舗を考えてみてください。あなたは列に並んで彼にアイテムを要求し、彼はそれをあなたに持ってきて、あなたは列から出ます。複数のアイテムが必要な場合は、覚えている限り、各ラウンドトリップでできるだけ多くのアイテムを受け取ることができます。

これを考えてみましょう。店員が1人いる店で、店員が列の一番前まで来たら、店員に「トイレットペーパーはありますか」と尋ねると、「はい」と言ったら、「OK、私は必要な量がわかったら、後で連絡します」と表示されたら、列の最前列に戻るまでに、売り切れになる可能性があります。このシナリオは、スレッドセーフなコレクションによって防止されません。

スレッドセーフなコレクションは、複数のスレッドからアクセスされた場合でも、その内部データ構造が常に有効であることを保証します。

スレッドセーフでないコレクションには、そのような保証はありません。たとえば、あるスレッドのバイナリツリーに何かを追加したときに、別のスレッドがツリーの再調整でビジーである場合、アイテムが追加される保証はなく、その後もツリーがまだ有効である場合でも、希望を超えて破損している可能性があります。

ただし、スレッドセーフなコレクションは、スレッドの順次操作がすべて内部データ構造の同じ「スナップショット」で機能することを保証しません。つまり、次のようなコードがある場合、

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

tree.Countとの間にtree.First()、別のスレッドがツリー内の残りのノードをクリアしたため、NullReferenceExceptionが発生する可能性があります。つまり、First()が返されnullます。

このシナリオでは、問題のコレクションに必要なものを取得する安全な方法があるかどうかを確認する必要があります。おそらく、上記のコードを書き直す必要があるか、ロックする必要があるかもしれません。


9
私はこの記事を読むたびに、それがすべて本当であるにもかかわらず、どういうわけか私たちは間違った道を進んでいます。
scope_creep

19
スレッド対応アプリケーションの主な問題は、可変データ構造です。あなたがそれを避けることができれば、あなたはあなた自身のトラブルの山を救うでしょう。
Lasse V. Karlsen、2009

89
これはスレッドセーフの良い説明ですが、私は仕方がありませんが、これは実際にはOPの質問に対処しなかったように感じます。OPが尋ねたのは(そして私が後でこの質問に出くわしたとき)、標準Dictionaryを使用することと、自分でロックを処理するConcurrentDictionaryことと、.NET 4+に組み込まれているタイプを使用することの違いでした。これが受け入れられたことには、実際には少し困惑しています。
mclark1129 2013

4
スレッドセーフは、適切なコレクションを使用するだけではありません。適切なコレクションを使用することが開始であり、対処しなければならない唯一のものではありません。それがOPが知りたかったことだと思います。なぜこれが受け入れられたのかはわかりません。これを書いてからしばらく経ちました。
ラッセV.カールセン2013

2
@ LasseV.Karlsenが言おうとしていることは、スレッドの安全性は簡単に達成できると思いますが、その安全性を処理する方法で並行性のレベルを指定する必要があります。「オブジェクトをスレッドセーフに保ちながら、同時により多くの操作を実行できますか?」と自問してください。次の例を検討してください。2人の顧客が異なる商品を返品したいと考えています。あなたがマネージャーとして、あなたの店に在庫を補充するために旅行をするとき、1つの旅行で両方の商品を補充して、それでも両方の顧客の要求を処理することはできませんか?
アレクサンドル

68

スレッドセーフがすべてのスレッドの問題を無視できるわけではないため、スレッドセーフコレクションを使用する場合は、十分に注意する必要があります。コレクションがそれ自体をスレッドセーフとしてアドバタイズする場合、通常は、複数のスレッドが同時に読み書きしている場合でも、コレクションが一貫した状態にあることを意味します。ただし、これは、単一のスレッドが複数のメソッドを呼び出した場合に、「論理的な」一連の結果が表示されるという意味ではありません。

たとえば、最初にキーが存在するかどうかを確認し、後でそのキーに対応する値を取得する場合、そのキーはConcurrentDictionaryバージョンでも存在しない可能性があります(別のスレッドがキーを削除した可能性があるため)。この場合でも、ロックを使用する必要があります(または、それ以上:TryGetValueを使用して2つの呼び出しを結合します)。

したがって、それらを使用してください。ただし、すべての同時実行の問題を無視するための無料パスが提供されるとは思わないでください。あなたはまだ注意する必要があります。


4
基本的に、オブジェクトを正しく使用しないと機能しないと言っていますか?
ChaosPandion 2009

3
基本的に、一般的なContains-> Addではなく、TryAddを使用する必要があります。
ChaosPandion 2009

3
@ブルーノ:いいえ。ロックされていない場合、別のスレッドが2つの呼び出しの間のキーを削除した可能性があります。
マーク・バイアーズ

13
ここで悪口を言うつもりはありませんTryGetValueが、常に一部でDictionaryあり、常に推奨されるアプローチでした。ConcurrentDictionary導入する重要な「並行」メソッドはAddOrUpdate、とGetOrAddです。だから、良い答えですが、もっと良い例を選ぶことができたでしょう。
アーロンノート、2009

4
正しい-トランザクションを含むデータベースとは異なり、ほとんどの同時メモリ内検索構造は分離の概念を欠いており、内部データ構造が正しいことのみを保証します。
チャン

39

内部的には、ConcurrentDictionaryはハッシュバケットごとに個別のロックを使用します。単一のエントリで機能するAdd / TryGetValueなどのメソッドのみを使用する限り、ディクショナリはほぼロックフリーのデータ構造として機能し、それぞれの優れたパフォーマンス上の利点があります。OTOH列挙メソッド(Countプロパティを含む)はすべてのバケットを一度にロックするため、同期されたディクショナリよりもパフォーマンスが低下します。

ただ、ConcurrentDictionaryを使用するだけです。


15

ConcurrentDictionary.GetOrAddメソッドは、ほとんどのマルチスレッドシナリオに必要なものだと思います。


1
私もTryGetを使用します。そうしないと、キーが既に存在するたびに、GetOrAddの2番目のパラメーターを初期化する労力が無駄になります。
ジョリットシッパーズ2013

7
GetOrAddにはオーバーロードGetOrAdd(TKey、Func <TKey、TValue>)があるため、キーが辞書に存在しない場合にのみ、2番目のパラメーターを遅延初期化できます。ほかTryGetValue変更しない、データのフェッチに使用されます。これらの各メソッドへの後続の呼び出しにより、2つの呼び出しの間に別のスレッドがキーを追加または削除した可能性があります。
sgnsajgon 2014年

ラッセの投稿は素晴らしいですが、これは質問に対する顕著な答えになるはずです。
3

13

.Net 3.5sp1のReactive Extensionsを見たことがありますか。Jon Skeetによると、彼らは.Net3.5 sp1の並列拡張と並行データ構造のバンドルをバックポートしました。

.Net 4 Beta 2のサンプルセットがあり、並列拡張機能の使用方法についてかなり詳しく説明しています。

先週、32スレッドを使用してI / Oを実行するConcurrentDictionaryをテストしたところです。これは宣伝どおりに機能しているようで、膨大な量のテストが行​​われていることを示しています。

編集:.NET 4 ConcurrentDictionaryとパターン。

マイクロソフトは、Parallellプログラミングのパターンと呼ばれるPDFをリリースしました。.Net 4同時拡張機能で使用する適切なパターンと回避すべきアンチパターンについて詳しく説明しているので、ダウンロードする価値は十分にあります。ここにあります。


4

基本的には、新しいConcurrentDictionaryを使います。箱から出してすぐに、スレッドセーフなプログラムを作成するために必要なコードを少なくする必要があります。


Concurrent Dictioneryを続行しても問題はありますか?
kbvishnu 2012年

1

スレッドセーフのニーズConcurrentDictionaryすべて満たす場合、これは優れたオプションです。そうでない場合、つまり、少し複雑なことをしている場合は、通常のDictionary+ lockの方が良いオプションです。たとえば、いくつかの注文を辞書に追加していて、注文の合計金額を更新し続けたいとします。次のようなコードを書くことができます:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

このコードはスレッドセーフではありません。複数のスレッドが_totalAmountフィールドを更新すると、フィールドが破損した状態になる可能性があります。だからあなたはそれでそれを保護しようとするかもしれませんlock

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

このコードは「安全」ですが、スレッドセーフではありません。が_totalAmountディクショナリのエントリと一致しているという保証はありません。別のスレッドがこれらの値を読み取って、UI要素を更新しようとする場合があります。

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

totalAmount辞書に注文の数に対応しない場合があります。表示された統計情報が間違っている可能性があります。この時点lockで、ディクショナリの更新を含めるように保護を拡張する必要があることがわかります。

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

このコードは完全に安全ですが、aを使用することの利点はすべてConcurrentDictionaryなくなりました。

  1. Dictionary内部の内部ロックConcurrentDictionaryが無駄になり、冗長になるため、通常のを使用するよりもパフォーマンスが低下します。
  2. シェア変数の保護されていない使用については、すべてのコードを確認する必要があります。
  3. ぎこちないAPI(TryAdd?、AddOrUpdate?)の使用に悩まされています。

だから私のアドバイスは:Dictionary+ lockから始めて、ConcurrentDictionaryこのオプションが実際に実行可能な場合は、パフォーマンスの最適化として後でaにアップグレードするオプションを保持します。多くの場合、そうではありません。


0

私たちは、再移入され、キャッシュされたコレクションのためのConcurrentDictionaryごとに1時間を使用して、類似した複数のクライアントスレッドで読んだソリューションにするために、この例のスレッド安全ですか?質問。

これをReadOnlyDictionaryに変更すると、 全体的なパフォーマンスが向上することがわかりました  。


ReadOnlyDictionaryは、別のIDictionaryのラッパーにすぎません。内部で何を使用していますか?
Lu55 2016年

@ Lu55、私たちはMS .Netライブラリを使用しており、利用可能な/適用可能な辞書の中から選択しました。私はReadOnlyDictionaryのMS内部実装について知らない
Michael Freidgeim
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.