List.Add()スレッドセーフ


87

一般に、リストはスレッドセーフではないことを理解していますが、スレッドがリストに対して他の操作(トラバースなど)を実行しない場合、単にアイテムをリストに追加することに何か問題がありますか?

例:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});


私はかつて、並行して実行される複数のタスクから新しいオブジェクトを追加するためにのみList <T>を使用していました。非常にまれなことですが、すべてのタスクが完了した後にリストを反復処理すると、レコードがnullになり、追加のスレッドが含まれていなければ、これを行うことは事実上不可能でした。リストが拡張するためにその要素を内部的に再割り当てしていたとき、どういうわけか別のスレッドが別のオブジェクトを追加しようとしてリストを台無しにしたと思います。したがって、そうするのは良い考えではありません!
osmiumbin

複数のスレッドから単純に追加したときに、オブジェクトが不可解にnullになることに関して、現在@osmiumbinが表示しているものとまったく同じです。確認していただきありがとうございます。
Blackey

回答:


74

舞台裏では、バッファの再割り当てや要素のコピーなど、多くのことが起こります。そのコードは危険を引き起こします。非常に簡単に言えば、リストに追加するときにアトミック操作はありません。少なくとも「Length」プロパティを更新する必要があり、アイテムを適切な場所に配置する必要があります。また、(別の変数がある場合は)インデックスが必要です。更新されます。複数のスレッドが互いに踏みにじることができます。そして、成長が必要な場合は、さらに多くのことが起こっています。何かがリストに書き込んでいる場合、他に何もリストに読み書きしてはなりません。

.NET 4.0には同時コレクションがあり、スレッドセーフであり、ロックを必要としません。


それは完全に理にかなっています、私は間違いなくこれのための新しい並行コレクションを見ていきます。ありがとうございました。
e36M3 2011

11
組み込み型はないことに注意してくださいConcurrentList。同時バッグ、辞書、スタック、キューなどがありますが、リストはありません。
LukeH 2011

11

現在のアプローチはスレッドセーフではありません-これは完全に回避することをお勧めします-基本的にデータ変換を行うため、PLINQの方が良いアプローチかもしれません(これは単純化された例ですが、最終的には各トランザクションを別の「状態」に投影しています「オブジェクト)。

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();

List.Addの側面を強調するために、過度に単純化された例を示しました。私のParallel.Foreachは実際にはかなりの量の作業を行い、単純なデータ変換ではありません。ありがとう。
e36M3 2011

4
並行コレクションは、不要に使用すると並列パフォーマンスを損なう可能性があります-固定サイズの配列を使用し、Parallel.Foreachインデックスを取り込むオーバーロードを使用することもできます-その場合、各スレッドは異なる配列エントリを操作しているため、安全である必要があります。
BrokenGlass 2011

6

尋ねるのは不合理なことではありません。そこている彼らはと呼ばれる唯一の方法であれば、他の方法と組み合わせてスレッド安全性の問題を引き起こす可能性があります方法は安全であるケースが。

ただし、リフレクターに示されているコードを検討すると、これは明らかにそうではありません。

public void Add(T item)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    this._items[this._size++] = item;
    this._version++;
}

EnsureCapacityそれ自体がスレッドセーフであったとしても(そしてそれは間違いなくそうではありません)、インクリメント演算子への同時呼び出しが誤書き込みを引き起こす可能性を考慮すると、上記のコードは明らかにスレッドセーフにはなりません。

ロックするか、ConcurrentListを使用するか、複数のスレッドが書き込みを行う場所としてロックフリーキューを使用し、作業が完了した後、直接またはリストにデータを入力して、そこから読み取ります(質問から判断すると、複数の同時書き込みとそれに続くシングルスレッド読み取りがここでのパターンです。そうしないと、Add呼び出される唯一のメソッドである条件がどのように役立つかわかりません)。


6

List.add複数のスレッドから使用したいが、順序を気にしない場合は、おそらく、のインデックス作成機能は必要ありません。Listとにかく代わりに使用可能な同時コレクションの一部を使用する必要があります。

このアドバイスを無視して実行するだけのadd場合、addスレッドセーフにすることができますが、次のように予測できない順序になります。

private Object someListLock = new Object(); // only once

...

lock (someListLock)
{
    someList.Add(item);
}

この予測できない順序を受け入れる場合は、前述のように、のようにインデックスを作成できるコレクションは必要ない可能性がありますsomeList[i]


5

リストは配列上に構築され、スレッドセーフではないため、スレッドの場所によっては、インデックスが範囲外の例外になるか、一部の値が他の値を上書きする可能性があるため、これにより問題が発生します。基本的に、それをしないでください。

複数の潜在的な問題があります...ただしないでください。スレッドセーフなコレクションが必要な場合は、ロックまたはSystem.Collections.Concurrentコレクションの1つを使用してください。


5

ConcurrentBag<T>List<T>このような代わりに使用して問題を解決しました:

ConcurrentBag<object> list = new ConcurrentBag<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

2

スレッドがリストで他の操作を実行しない場合、単にアイテムをリストに追加することに何か問題がありますか?

簡単な答え:はい。

長い答え:以下のプログラムを実行してください。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

class Program
{
    readonly List<int> l = new List<int>();
    const int amount = 1000;
    int toFinish = amount;
    readonly AutoResetEvent are = new AutoResetEvent(false);

    static void Main()
    {
        new Program().Run();
    }

    void Run()
    {
        for (int i = 0; i < amount; i++)
            new Thread(AddTol).Start(i);

        are.WaitOne();

        if (l.Count != amount ||
            l.Distinct().Count() != amount ||
            l.Min() < 0 ||
            l.Max() >= amount)
            throw new Exception("omg corrupted data");

        Console.WriteLine("All good");
        Console.ReadKey();
    }

    void AddTol(object o)
    {
        // uncomment to fix
        // lock (l) 
        l.Add((int)o);

        int i = Interlocked.Decrement(ref toFinish);

        if (i == 0)
            are.Set();
    }
}

@royiこれをシングルコアマシンで実行していますか?
バススミット2014

こんにちは。この例では、数値1000が見つかるたびにAutoResetEventを設定しているため、問題があると思います。たとえば、これらのスレッドは必要なときにいつでも処理できるため、999に達する前に1000に達する可能性があります。AddTolメソッドにConsole.WriteLineを追加すると、番号が正しくないことがわかります。
デイブウォーカー

@ dave、i == 0の場合のイベントの設定
Bas Smit

2

他の人がすでに言ったように、あなたはからの同時コレクションを使うことができます System.Collections.Concurrent名前空間ます。それらのいずれかを使用できる場合は、これが推奨されます。

ただし、同期されたばかりのリストが本当に必要な場合は、のSynchronizedCollection<T>-Classを参照してくださいSystem.Collections.Generic

System.ServiceModelアセンブリを含める必要があることに注意してください。これが、私があまり好きではない理由でもあります。しかし時々私はそれを使います。


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