5

この質問に関係のないバグを探していくつかのデータベースList<T>コードを調べているときに、いくつかの場所で不適切に使用されていることに気付きました。具体的には:

  1. Listas リーダーに同時にアクセスしている多くのスレッドがありましたが、 の代わりにへのインデックスを使用していました。listenumerators
  2. への単一のライターがありましたlist
  3. 同期はゼロで、リーダーとライターはlist同時に にアクセスしていましたが、コード構造のため、を実行したメソッドが返されるまで、最後の要素にアクセスすることはありませんでした。Add()
  4. から削除された要素はありませんlist

C#のドキュメントによると、これはスレッド セーフではありません。それでも失敗したことはありません。の特定の実装のためにList(内部的には、スペースが不足したときに再割り当てする配列であると想定しています)、1-writer 0-enumerator n-reader add-only シナリオが誤ってスレッドセーフになっているのではないかと思っています。または、現在の.NET4実装でこれが爆発する可能性が低いシナリオはありますか?

編集:いくつかの返信を読むのを忘れた重要な詳細。リーダーは、Listとその内容を読み取り専用として扱います。

4

5 に答える 5

2

これは吹き飛ばすことができます。まだしていないだけです。古くなったインデックスは、通常、最初に削除されます。吹かしたくないときだけ吹く。あなたはおそらく今のところ幸運です。

.Net 4.0 を使用しているので、スレッド セーフであることが保証されている System.Collections.Concurrent から適切なコレクションにリストを変更することをお勧めします。また、配列インデックスの使用を避け、何かを検索する必要がある場合は ConcurrentDictionary に切り替えます。

http://msdn.microsoft.com/en-us/library/dd287108.aspx

于 2011-07-31T21:03:30.037 に答える
1

失敗したことがないか、アプリケーションがクラッシュしないため、このシナリオがスレッド セーフであるとは限りません。たとえば、ライター スレッドがリスト内のフィールドを更新するとします。それがlongフィールドであると同時に、リーダー スレッドがそのフィールドを読み取ります。返される値は、古いフィールドと新しいフィールドの 2 つのフィールドのビットごとの組み合わせかもしれません! これは、リーダー スレッドがメモリから値の読み取りを開始し、読み取りが完了する前にライター スレッドが値を更新したために発生する可能性があります。

編集:もちろん、リーダースレッドが何も更新せずにすべてのデータを読み取ると仮定すると、それらは配列自体の値を変更しないと確信していますが、それらはプロパティまたはフィールド内のフィールドを変更する可能性があります彼らが読んだ値。例えば:

for (int index =0 ; index < list.Count; index++)
{
    MyClass myClass = list[index];//ok we are just reading the value from list
    myClass.SomeInteger++;//boom the same variable will be updated from another threads...
}

この例は、リストが公開した共有変数ではなく、リスト自体のスレッドセーフについては話していません。

結論としては、lockライターが 1 つしかなく、アイテムが削除されていない場合でも、リストとのやり取りの前などに同期メカニズムを使用する必要があるということです。これにより、そもそも必要のない小さなバグや失敗のシナリオを防ぐことができます。

于 2011-07-31T20:50:46.153 に答える
0

まず、ドキュメントが信頼できるのはいつからですか?

第二に、この答えは、OPの詳細よりも一般的な質問に対するものです。

私は理論的にはMrFoxに同意します。なぜなら、これはすべて2つの質問に要約されるからです。

  1. Listクラスはフラット配列として実装されていますか?

はいの場合、次のようになります。

  1. 書き込みの途中で書き込み命令をプリエンプトできますか>

これは当てはまらないと思います。完全な書き込みは、DWORDなどを読み取る前に行われます。つまり、DWORDの4バイトのうち2バイトを書き込んだ後、新しい値の1/2と古い値の1/2を読み取ることは決してありません。

したがって、ポインタにオフセットを指定して配列にインデックスを付ける場合は、スレッドロックなしで安全に読み取ることができます。リストが単なるポインタ計算以上のことをしている場合、それはスレッドセーフではありません。

リストがフラット配列を使用していなかったとしたら、今ではクラッシュするのを見たことがあると思います。

私自身の経験では、スレッドをロックせずに、インデックスを介してリストから1つのアイテムを安全に読み取ることができます。これはすべて私見ですが、それだけの価値があると考えてください。

リストを反復処理する必要がある場合など、最悪の場合、最善の方法は次のとおりです。

  1. リストをロックする
  2. 同じサイズの配列を作成する
  3. CopyTo()を使用して、リストを配列にコピーします
  4. リストのロックを解除する
  5. 次に、リストの代わりに配列を繰り返し処理します。

(.netと呼んでいるものは何でも)C ++:

  List<Object^>^ objects = gcnew List<Object^>^();
  // in some reader thread:
  Monitor::Enter(objects);
  array<Object^>^ objs = gcnew array<Object^>(objects->Count);
  objects->CopyTo(objs);
  Monitor::Exit(objects);
  // use objs array

メモリ割り当てがあっても、これはリストをロックしてロックを解除する前にすべてを繰り返すよりも高速です。

Just a heads up though: if you want a fast system, thread-locking is your worst enemy. Use ZeroMQ instead. I can speak from experience, message-based synch is the right way to go.

于 2012-01-17T03:02:36.530 に答える
0

したがって、アーキテクチャが 32 ビットの場合、long や double などの 32 ビットより大きいフィールドへの書き込みは、スレッド セーフな操作ではありません。System.Doubleのドキュメントを参照してください。

この型のインスタンスの割り当ては、すべてのハードウェア プラットフォームでスレッド セーフではありません。これは、そのインスタンスのバイナリ表現が大きすぎて 1 回のアトミック操作で割り当てることができない場合があるためです。

ただし、リストのサイズが固定されている場合、この状況は、List が 32 ビットを超える値の型を格納している場合にのみ問題になります。リストが参照型のみを保持している場合、スレッド セーフの問題は参照型自体に起因しており、それらの格納やリストからの取得に起因するものではありません。たとえば、不変の参照型は、可変の参照型よりもスレッド セーフの問題を引き起こす可能性が低くなります。

さらに、List の実装の詳細を制御することはできません。そのクラスは主にパフォーマンスのために設計されており、スレッド セーフではなく、その側面を考慮して将来変更される可能性があります。

特に、リストに要素を追加したり、リストのサイズを変更したりすることは、リストの要素が 32 ビット長であってもスレッドセーフではありません。リストに要素を配置するだけでなく、挿入、追加、または削除が必要になるためです。他のスレッドがリストにアクセスした後にそのような操作が必要な場合は、リストへのアクセスをロックするか、同時リスト実装を使用することをお勧めします。

于 2011-07-31T21:03:37.477 に答える
0

スレッド セーフは、データが一度に複数回変更された場合にのみ重要になります。読者数は問いません。誰かが読んでいる間に誰かが書いている場合でも、リーダーは古いデータまたは新しいデータを取得しますが、それでも機能します。Add() が返された後にのみ要素にアクセスできるという事実は、要素の一部が個別に読み取られることを防ぎます。Insert() メソッドの使用を開始すると、リーダーが間違ったデータを取得する可能性があります。

于 2011-07-31T21:11:28.223 に答える