8

System.Collections.Generic.List<T>タイマー コールバックでのみアイテムを追加する があります。タイマーは、操作が完了した後にのみ再開されます。

System.Collections.Concurrent.ConcurrentQueue<T>上記のリストに追加されたアイテムのインデックスを格納するがあります。このストア操作も、常に上記の同じタイマー コールバックで実行されます。

キューを反復処理し、リスト内の対応するアイテムにアクセスする読み取り操作はスレッド セーフですか?

サンプルコード:

private List<Object> items;
private ConcurrentQueue<int> queue;
private Timer timer;
private void callback(object state)
{
    int index = items.Count;
    items.Add(new object());
    if (true)//some condition here
        queue.Enqueue(index);
    timer.Change(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(-1));
}

//This can be called from any thread
public IEnumerable<object> AccessItems()
{
    foreach (var index in queue)
    {
        yield return items[index];
    }
}

私の理解: インデックス作成中にリストのサイズが変更されたとしても、既に存在するアイテムにアクセスしているだけなので、古い配列から読み取られたのか、新しい配列から読み取られたのかは関係ありません。したがって、これはスレッドセーフである必要があります。

4

4 に答える 4

9

キューを反復処理し、リスト内の対応するアイテムにアクセスする読み取り操作はスレッド セーフですか?

スレッドセーフであると文書化されていますか?

そうでない場合、たとえそれが偶然にこの実装に含まれていたとしても、それをスレッドセーフとして扱うのはばかげています。スレッド セーフは設計によるものです

スレッド間でメモリを共有することは、そもそも悪い考えです。そうしない場合は、操作がスレッドセーフかどうかを確認する必要はありません。

それを行う必要がある場合は、共有メモリ アクセス用に設計されたコレクションを使用します。

それができない場合は、 lock を使用してください。競合がなければ、ロックは安価です。

ロックが常に競合しているためにパフォーマンスの問題が発生している場合は、ローロック コードのような危険で愚かなことをしようとするのではなく、スレッド アーキテクチャを変更して問題を解決してください。少数の専門家を除いて、誰もローロック コードを正しく記述できません。(私はその一人ではありません。ローロック コードも書いていません。)

インデックス作成中にリストのサイズが変更されたとしても、既に存在するアイテムにアクセスしているだけなので、古い配列から読み取られたのか、新しい配列から読み取られたのかは関係ありません。

それはそれについて考える間違った方法です。それについて考える正しい方法は次のとおりです。

リストのサイズが変更された場合、リストの内部データ構造が変更されています。ミューテーションの途中で内部データ構造が一貫性のない形式に変更される可能性がありますが、ミューテーションが終了するまでには一貫性が保たれます。したがって、読者は別のスレッドからこの一貫性のない状態を見ることができ、プログラム全体の動作が予測不能になります。クラッシュする可能性があり、無限ループに陥る可能性があり、他のデータ構造が破損する可能性があります。わかりません。一貫性のない状態の世界で一貫した状態を想定するコードを実行しているためです。

于 2013-02-24T15:46:36.197 に答える
6

大きな編集

ConcurrentQueue は、Enqueue(T) および T Dequeue() 操作に関してのみ安全です。その上でforeachを実行していますが、必要なレベルで同期されません。特定のケースでの最大の問題は、キュー (それ自体がコレクションである) を列挙すると、よく知られている「コレクションが変更されました」という例外がスローされる可能性があるという事実です。なぜそれが最大の問題なのですか?後でキューに物を追加しているため対応するオブジェクトをリストに追加しました(リストを同期する必要性も大いにありますが、最大の問題はたった1つの「弾丸」で解決されます)。コレクションを列挙している間、別のスレッドがそれを変更しているという事実を飲み込むのは簡単ではありません (顕微鏡レベルで変更が安全であっても - ConcurrentQueue はまさにそれを行います)。

したがって、別の同期手段を使用して、キュー (およびそこにいる間は中央のリスト) へのアクセスを同期する必要があります (つまり、ConcurrentQueue を忘れて、単純なキューまたはリストを使用することもできます。決してデキューしないでください)。

だから、次のようなことをしてください:

public void Writer(object toWrite) {
  this.rwLock.EnterWriteLock();
  try {
    int tailIndex = this.list.Count;
    this.list.Add(toWrite);

    if (..condition1..)
      this.queue1.Enqueue(tailIndex);
    if (..condition2..)
      this.queue2.Enqueue(tailIndex);
    if (..condition3..)
      this.queue3.Enqueue(tailIndex);
    ..etc..
  } finally {
    this.rwLock.ExitWriteLock();
  }
}

そしてAccessItemsで:

public IEnumerable<object> AccessItems(int queueIndex) {
  Queue<object> whichQueue = null;
  switch (queueIndex) {
    case 1: whichQueue = this.queue1; break;
    case 2: whichQueue = this.queue2; break;
    case 3: whichQueue = this.queue3; break;
    ..etc..
    default: throw new NotSupportedException("Invalid queue disambiguating params");    
  }

  List<object> results = new List<object>();
  this.rwLock.EnterReadLock();
  try {
    foreach (var index in whichQueue)
      results.Add(this.list[index]);
  } finally {
    this.rwLock.ExitReadLock();
  }

  return results;
}

そして、あなたのアプリがリストやさまざまなキューにアクセスするケースについての私の完全な理解に基づいて、それは 100% 安全であるはずです.

大編集終了

まず第一に、あなたが Thread-Safe と呼んでいるものは何ですか? エリック・リッパート著

あなたの特定のケースでは、答えはノーだと思います。

グローバル コンテキスト (実際のリスト) で矛盾が発生する可能性はありません。

代わりに、実際のリーダー (一意のライターと「衝突」する可能性が非常に高い) が、それ自体に矛盾が生じる可能性があります (独自のスタックの意味: すべてのメソッドのローカル変数、パラメーター、およびヒープの論理的に分離された部分)。 )))。

このような「スレッドごと」の不一致の可能性 (N 番目のスレッドはリスト内の要素の数を学習したいため、値が 39404999 であることがわかりますが、実際には 3 つの値しか追加されていません) は、一般的にそのアーキテクチャについて言えば、それを宣言するのに十分です。スレッドセーフではありません(ただし、グローバルにアクセス可能なリストを実際に変更することはありませんが、単に欠陥のある方法で読み取るだけです)。

ReaderWriterLockSlimクラスを使用することをお勧めします。あなたのニーズに合っていることがわかると思います:

private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

private List<Object> items;
private ConcurrentQueue<int> queue;
private Timer timer;
private void callback(object state)
{
  int index = items.Count;

  this.rwLock.EnterWriteLock();
  try {
    // in this place, right here, there can be only ONE writer
    // and while the writer is between EnterWriteLock and ExitWriteLock
    // there can exist no readers in the following method (between EnterReadLock
    // and ExitReadLock)

    // we add the item to the List
    // AND do the enqueue "atomically" (as loose a term as thread-safe)
    items.Add(new object());

    if (true)//some condition here
      queue.Enqueue(index);
  } finally {
    this.rwLock.ExitWriteLock();
  }

  timer.Change(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(-1));
}

//This can be called from any thread
public IEnumerable<object> AccessItems()
{
  List<object> results = new List<object>();

  this.rwLock.EnterReadLock();
  try {
    // in this place there can exist a thousand readers 
    // (doing these actions right here, between EnterReadLock and ExitReadLock)
    // all at the same time, but NO writers
    foreach (var index in queue)
    {
      this.results.Add ( this.items[index] );
    }        
  } finally {
    this.rwLock.ExitReadLock();
  }


  return results; // or foreach yield return you like that more :)
}
于 2013-02-24T14:19:13.490 に答える
1

同じオブジェクトに対して同時に読み取りと書き込みを行っているため、いいえ。これは安全であると文書化されていないため、安全であるかどうかを確認することはできません。しないでください。

.NET 4.0の時点で実際に安全ではないという事実は、何の意味もありません。リフレクターによると安全だったとしても、いつでも変わる可能性があります。現在のバージョンに依存して将来のバージョンを予測することはできません。

このようなトリックで逃げようとしないでください。明らかに安全な方法でそれを行わないのはなぜですか?

補足:2つのタイマーコールバックが同時に実行される可能性があるため、コードが二重に壊れています(複数のライター)。スレッドでトリックをやってのけることを試みないでください。

于 2013-02-24T15:15:27.577 に答える
1

スレサフィッシュです。foreach ステートメントは ConcurrentQueue.GetEnumerator() メソッドを使用します。どの約束:

列挙は、キューの内容の瞬間的なスナップショットを表します。GetEnumerator が呼び出された後のコレクションへの更新は反映されません。列挙子は、キューからの読み取りおよびキューへの書き込みと同時に安全に使用できます。

これは、Queue クラスを使用したときに得られる類のような不可解な例外メッセージで、プログラムが無作為に爆発することはないと言う別の言い方です。ただし、結果に注意してください。この保証に暗示されているのは、キューの古いバージョンを見ている可能性があるということです。ループは、ループの実行開始後に別のスレッドによって追加された要素を認識できなくなります。そのような魔法は存在せず、一貫した方法で実装することは不可能です。それがあなたのプログラムの誤動作を引き起こすかどうかは、あなたが考えなければならないことであり、質問から推測することはできません. 完全に無視できることはほとんどありません。

ただし、 List<> の使用はまったく安全ではありません。

于 2013-02-24T16:03:02.007 に答える