25

リスト コレクションがあり、それをマルチ スレッド アプリで反復処理したいと考えています。変更される可能性があり、foreachを実行するときに「コレクションが変更されました」という例外が発生したくないため、反復するたびに保護する必要があります。

これを行う正しい方法は何ですか?

  1. アクセスまたはループするたびにロックを使用します。私はデッドロックがかなり怖いです。たぶん、私はロックを使用することに偏執的であり、そうすべきではありません。デッドロックを回避するためにこのルートを使用する場合、何を知る必要がありますか? ロックはかなり効率的ですか?

  2. foreach を実行するたびに List<>.ToArray() を使用して配列にコピーします。これはパフォーマンスに影響を与えますが、簡単に実行できます。コピーする時間だけでなく、メモリのスラッシングも心配です。ただ過剰に思えます。ToArray を使用するのはスレッド セーフですか?

  3. foreach を使用せず、代わりに for ループを使用してください。リストが縮小されていないことを確認するために、これを行うたびに長さチェックを行う必要はありませんか? それは迷惑そうです。

4

5 に答える 5

48

デッドロックを恐れる理由はほとんどありません。デッドロックは簡単に検出できます。あなたのプログラムは実行を停止します。あなたが本当に恐れるべきことは、レースをスレッド化することです。これは、ロックする必要があるときにロックしないと発生する種類のバグです。 診断が非常に難しい。

  1. lock の使用は問題ありません。そのリストに触れるすべてのコードで、まったく同じロック オブジェクトを使用していることを確認してください。そのリストにアイテムを追加または削除するコードのように。そのコードがリストを反復する同じスレッドで実行される場合、ロックは必要ありません。一般に、ここでデッドロックが発生する唯一の可能性は、Thread.Join() などのスレッド状態に依存するコードがあり、そのロック オブジェクトも保持している場合です。これは珍しいはずです。

  2. はい、ToArray() メソッドの周りでロックを使用している限り、リストのコピーの反復は常にスレッドセーフです。まだロックが必要であり、構造的な改善はないことに注意してください。利点は、ロックを短時間保持して、プログラムの同時実行性を向上させることです。欠点は、O(n) のストレージ要件、セーフ リストのみを持ち、リスト内の要素を保護しないこと、およびリスト コンテンツの古いビューを常に保持するというトリッキーな問題です。特に最後の問題は微妙で分析が難しい。副作用を推論できない場合は、おそらくこれを考慮すべきではありません。

  3. 人種を検出する foreach の機能は、問題ではなく贈り物として扱うようにしてください。はい、明示的な for(;;) ループは例外をスローしません。誤動作するだけです。同じアイテムを 2 回繰り返したり、アイテムを完全にスキップしたりするようなものです。逆方向に反復することで、アイテムの数を再確認する必要がなくなります。他のスレッドが Add() のみを呼び出しており、ToArray() と同様に動作する Remove() を呼び出していない限り、古いビューが表示されます。これが実際に機能するわけではなく、リストのインデックス作成もスレッドセーフではありません。List<> は、必要に応じて内部配列を再割り当てします。これは機能せず、予期しない方法で誤動作します。

ここには 2 つの視点があります。あなたはおびえ、一般的な知恵に従うことができます。それは賢明で、上司を喜ばせます。または、実験して、ルールをゆがめるとどのように問題が発生するかを自分で確認することもできます. それはあなたを幸せにするでしょう、あなたははるかに優れたプログラマーになるでしょう. しかし、生産性は低下します。あなたのスケジュールがどのように見えるかわかりません。

于 2010-06-27T22:28:45.037 に答える
11

List データがほとんど読み取り専用の場合、 ReaderWriterLockSlimを使用して、複数のスレッドが同時に安全にアクセスできるようにすることができます。

ここでスレッドセーフ辞書の実装を見つけることができます。

また、.Net 4.0 を使用している場合、BlockingCollectionクラスがこの機能を自動的に実装することにも言及したいと思います。数ヶ月前にこれを知っていたらよかったのに!

于 2010-06-27T21:46:32.487 に答える
5

不変のデータ構造の使用を検討することもできます-リストを値型のように扱います。

可能であれば、イミュータブル オブジェクトを使用することは、マルチスレッド プログラミングにとって優れた選択肢となる可能性があります。基本的に、オブジェクトの状態を変更する操作は、まったく新しいオブジェクトを作成します。

たとえば、アイデアを示すために次のように書きました。決して参照コードではないことをお詫びします。また、少し長くなり始めました。

public class ImmutableWidgetList : IEnumerable<Widget>
{
    private List<Widget> _widgets;  // we never modify the list

    // creates an empty list
    public ImmutableWidgetList()
    {
        _widgets = new List<Widget>();
    }

    // creates a list from an enumerator
    public ImmutableWidgetList(IEnumerable<Widget> widgetList)
    {
        _widgets = new List<Widget>(widgetList);
    }

    // add a single item
    public ImmutableWidgetList Add(Widget widget)
    {
        List<Widget> newList = new List<Widget>(_widgets);

        ImmutableWidgetList result = new ImmutableWidgetList();
        result._widgets = newList;
        return result;
    }

    // add a range of items.
    public ImmutableWidgetList AddRange(IEnumerable<Widget> widgets)
    {
        List<Widget> newList = new List<Widget>(_widgets);
        newList.AddRange(widgets);

        ImmutableWidgetList result = new ImmutableWidgetList();
        result._widgets = newList;
        return result;
    }

    // implement IEnumerable<Widget>
    IEnumerator<Widget> IEnumerable<Widget>.GetEnumerator()
    {
        return _widgets.GetEnumerator();
    }


    // implement IEnumerable
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _widgets.GetEnumerator();
    }
}
  • IEnumerable<T>実装を可能にするために含めましたforeach
  • 新しいリストを作成するときの空間/時間のパフォーマンスが心配だとおっしゃいましたが、おそらくこれはうまくいかないでしょう。
  • あなたも実装したいかもしれませんIList<T>
于 2010-06-29T02:15:26.610 に答える
4

一般に、ハッシュ テーブルを除いて、コレクションはパフォーマンス上の理由からスレッド セーフではありません。IsSynchronized と SyncRoot を使用してスレッド セーフにする必要があります。こちらとこちらをご覧ください

msdn の例

ICollection myCollection = someCollection;
lock(myCollection.SyncRoot)
{
    foreach (object item in myCollection)
    {
        // Insert your code here.
    }
}

編集:.net 4.0を使用している場合は、同時コレクションを使用できます

于 2010-06-27T21:45:05.563 に答える
3

lock()コピーを作成する別の理由がない限り使用してください。デッドロックは、次のように異なる順序で複数のロックを要求している場合にのみ発生する可能性があります。

スレッド 1:

lock(A) {
  // .. stuff
  // Next lock request can potentially deadlock with 2
  lock(B) {
    // ... more stuff
  }
}

スレッド 2:

lock(B) {
  // Different stuff
  // next lock request can potentially deadlock with 1
  lock(A) {
    // More crap
  }
}

ここで、スレッド 1 とスレッド 2 はデッドロックを引き起こす可能性があります。これAは、スレッド 2 が保持している間にスレッド 1 が保持している可能性がBあり、どちらも他方がそのロックを解放するまで続行できないためです。

複数のロックを取得する必要がある場合は、常に同じ順序で実行してください。ロックを 1 つだけ取得している場合、デッドロックは発生しません ... ユーザー入力を待機している間にロックを保持しない限り、それは技術的にはデッドロックではなく、別のポイントにつながります: ロックをそれ以上保持しないでください。あなたが絶対にしなければならないよりも。

于 2010-06-27T21:07:55.387 に答える