.Net 4.0にConcurrentList <T>はありませんか?


198

System.Collections.Concurrent.Net 4.0の新しい名前空間を見て、とても嬉しく思いました。私が見てきたConcurrentDictionaryConcurrentQueueConcurrentStackConcurrentBagおよびBlockingCollection

不思議なことに欠けているように見えるのは、ConcurrentList<T>です。私はそれを自分で書く必要がありますか(またはWebから取得する必要がありますか?))

ここに明らかなものがないのですか?



4
@ RodrigoReis、ConcurrentBag <T>は順序付けられていないコレクションですが、List <T>は順序付けされています。
Adam Calvet Bohl 2017

4
どうすればマルチスレッド環境で順序付けられたコレクションを作成できるでしょうか?設計上、要素のシーケンスを制御することはできません。
Jeremy Holovacs 2017

代わりにロックを使用してください
Erik Bergstedt

dotnetソースコードにThreadSafeList.csというファイルがあり、以下のコードのように見えます。また、ReaderWriterLockSlimを使用し、なぜ単純なlock(obj)の代わりにそれを使用するのかを理解しようとしていましたか?
コリンラマール

回答:


166

私はしばらく前にそれを試しaを与えた(も:GitHubの上で)。私の実装にはいくつかの問題がありましたが、ここでは説明しません。さらに重要なことに、私が学んだことをお話ししましょう。

まず、完全な実装をIList<T>ロックレスでスレッドセーフにする方法はありません。特に、ランダムな挿入と削除は行われませんあなたもO(1)ランダムアクセス(すなわち、あなたが「カンニング」とだけリンクリストのいくつかの並べ替えを使用してインデックスを吸う聞かせていない限り)を忘れていない限り、仕事に行きます。

私が思ったことが価値があるは、スレッドセーフの限定されたサブセットでしたIList<T>。特に、インデックスによるAddランダムな読み取り専用アクセスを許可し、提供します(ただし、、などは不可InsertRemoveAt、ランダムな書き込みアクセスも不可)。

これが私のConcurrentList<T>実装の目標でした。しかし、マルチスレッドシナリオでそのパフォーマンスをテストしたところ、単純に加算を同期するList<T>方が高速であることが。基本的に、aへの追加List<T>はすでに非常に高速です。関連する計算ステップの複雑さは非常に小さいです(配列の要素への増分索引およびアサイン;だと本当に)。これに関するあらゆる種類のロック競合を確認するには、大量の同時書き込みが必要になります。それでも、各書き込みの平均パフォーマンスは、でのロックレス実装ではありConcurrentList<T>ますが、より高価なものを打ち負かします。

リストの内部配列自体のサイズを変更する必要がある比較的まれなイベントでは、少額の費用を支払う必要があります。最終的に私はこれが 1つのアドオンのみニッチなシナリオConcurrentList<T>コレクション型は理にかなって:お好きな時に保証上の元素を添加する低オーバーヘッドのすべての単一の呼び出しを(そう、償却パフォーマンス目標ではなく)。

それは、あなたが思っているほどクラスの有用性が低いわけではありません。


52
そして、あなたがList<T>old-skool、モニターベースの同期を使用するようなものを必要とするならば、それSynchronizedCollection<T>はBCLに隠されています:msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH

8
1つの小さな追加:容量コンストラクターパラメーターを使用して、サイズ変更のシナリオを(可能な限り)回避します。
Henk Holterman、2011

2
ConcurrentListaが勝利する最大のシナリオは、リストに追加されているアクティビティがそれほど多くない場合ですが、同時に多くの読者がいます。読者のオーバーヘッドを1つのメモリバリアに減らすことができます(そして、読者が少し古いデータを気にしていなかったとしても、それを排除できます)。
スーパーキャット2013

2
@Kevin:ConcurrentList<T>比較的わずかなオーバーヘッドを追加するだけで、ロックを一切必要とせずに一貫した状態を閲覧者が保証できるような方法でを構築するのは非常に簡単です。リストがたとえばサイズ32から64に拡張されたら、サイズ32の配列を保持し、新しいサイズ64の配列を作成します。次の32個のアイテムをそれぞれ追加するときは、新しい配列の32〜63のスロットに挿入し、サイズ32の配列から新しい項目に古い項目をコピーします。64番目のアイテムが追加されるまで、リーダーはサイズ32の配列でアイテム0〜31を、サイズ64の配列でアイテム32〜63を検索します。
スーパーキャット2013

2
64番目のアイテムが追加されると、size-32配列は0〜31のアイテムを取得するために引き続き機能しますが、リーダーはそれを使用する必要がなくなります。すべてのアイテム0〜63にはサイズ64の配列を、アイテム64〜127にはサイズ128の配列を使用できます。2つの配列のどちらを使用するかを選択するオーバーヘッドと、必要に応じてメモリバリアを追加すると、考えられる最も効率的なリーダー/ライターロックのオーバーヘッドよりも少なくなります。書き込みは、おそらく1つが、すべての挿入で新しいオブジェクトインスタンスを作成する気にしなかったが、ロックが安いべきである場合は特に、ロックフリーが可能になります(ロックを使用する必要があります。
supercat

38

どのような場合にConcurrentListを使用しますか?

スレッド化された世界でのランダムアクセスコンテナーの概念は、見た目ほど便利ではありません。ステートメント

  if (i < MyConcurrentList.Count)  
      x = MyConcurrentList[i]; 

全体としてはまだスレッドセーフではありません。

ConcurrentListを作成する代わりに、そこにあるものを使用してソリューションを構築してみてください。最も一般的なクラスは、ConcurrentBag、特にBlockingCollectionです。


いい視点ね。それでも私がやっていることはもう少し平凡です。ConcurrentBag <T>をIList <T>に割り当てようとしています。プロパティをIEnumerable <T>に切り替えることはできましたが、それに.Addすることはできません。
アラン・

1
@アラン:リストをロックせずにそれを実装する方法はありません。Monitorとにかくそれを行うために既に使用できるので、同時リストの理由はありません。
Billy ONeal、2011

6
@dcp-はい、これは本質的にスレッドセーフではありません。ConcurrentDictionaryには、AddOrUpdate、GetOrAdd、TryUpdateなどの1つのアトミック操作でこれを実行する特別なメソッドがあります。ディクショナリーを変更せずにキーが存在するかどうかを知りたい場合があるため(HashSetを考えてください)
Zarat

3
@dcp-ContainsKeyはそれ自体がスレッドセーフです。最初の決定に応じて2回目の呼び出しを行うため、この例(ContainsKeyではありません)は競合状態になっています。
Zarat

2
ヘンク、同意しない。それは非常に役に立つかもしれない単純なシナリオがあると思います。それにワーカースレッドを書き込むと、それに応じてUIスレッドがインターフェイスを読み取り、更新します。ソートされた方法でアイテムを追加する場合は、ランダムアクセス書き込みが必要です。スタックとデータのビューを使用することもできますが、2つのコレクションを維持する必要があります:
(。– Eric Ouellet

19

すでに提供されているすばらしい回答にすべての敬意を払って、私は単にスレッドセーフなIListが必要な場合があります。高度なものや空想的なものはありません。多くの場合、パフォーマンスは重要ですが、それが問題になることはありません。はい、「TryGetValue」などのメソッドなしでは常に課題が発生しますが、ほとんどの場合、すべてにロックをかけることを心配する必要なく列挙できるものが必要です。そして、はい、おそらく誰かが私の実装でデッドロックまたは何かにつながる可能性のある「バグ」を見つける可能性がありますが(おそらく)、正直に言うと、マルチスレッドに関しては、コードを正しく記述しないと、とにかく行き詰まっている。そのことを念頭に置いて、これらの基本的なニーズを提供する単純なConcurrentList実装を作成することにしました。

そして、その価値のために:通常のListとConcurrentListに10,000,000のアイテムを追加する基本的なテストを行い、結果は次のとおりでした。

リストは7793ミリ秒で終了しました。同時終了:8064ミリ秒。

public class ConcurrentList<T> : IList<T>, IDisposable
{
    #region Fields
    private readonly List<T> _list;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructors
    public ConcurrentList()
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>();
    }

    public ConcurrentList(int capacity)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(capacity);
    }

    public ConcurrentList(IEnumerable<T> items)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(items);
    }
    #endregion

    #region Methods
    public void Add(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Add(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void Insert(int index, T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Insert(index, item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            return this._list.Remove(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void RemoveAt(int index)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.RemoveAt(index);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public int IndexOf(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.IndexOf(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void Clear()
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Clear();
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.Contains(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        try
        {
            this._lock.EnterReadLock();
            this._list.CopyTo(array, arrayIndex);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    ~ConcurrentList()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
            GC.SuppressFinalize(this);

        this._lock.Dispose();
    }
    #endregion

    #region Properties
    public T this[int index]
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list[index];
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
        set
        {
            try
            {
                this._lock.EnterWriteLock();
                this._list[index] = value;
            }
            finally
            {
                this._lock.ExitWriteLock();
            }
        }
    }

    public int Count
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list.Count;
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
    #endregion
}

    public class ConcurrentEnumerator<T> : IEnumerator<T>
{
    #region Fields
    private readonly IEnumerator<T> _inner;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructor
    public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
    {
        this._lock = @lock;
        this._lock.EnterReadLock();
        this._inner = inner.GetEnumerator();
    }
    #endregion

    #region Methods
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }

    public void Dispose()
    {
        this._lock.ExitReadLock();
    }
    #endregion

    #region Properties
    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return _inner.Current; }
    }
    #endregion
}

5
わかりました、古い答えですが、まだ: RemoveAt(int index)スレッドセーフになることInsert(int index, T item)はなく、index == 0の場合にのみ安全であり、の戻りIndexOf()はすぐに古くなりthis[int]ます。
Henk Holterman、2015

2
また、〜Finalizer()も必要ありません。
Henk Holterman、2015

2
あなたは、デッドロックの可能性を防ぐことをあきらめたと言っています- 同時に使用ReaderWriterLockSlimすることで、シングルを簡単にデッドロックさせることができますEnterUpgradeableReadLock()。ただし、これを使用したり、ロックを外部からアクセスできるようにしたり、読み取りロックを保持しているときに書き込みロックに入るメソッドを呼び出したりしないため、クラスを使用してもデッドロックは発生しませんおそらく。
Eugene Beresovsky、2015

1
非並行インターフェースは、並行アクセスには適していません。たとえば、以下はアトミックではありませんvar l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";。一般的に、読み取り/書き込みコンボを同時に実行すると、深刻な問題が発生する可能性があります。同時データ構造は、一般的にそれらのための方法を提供する理由は次のように、あるConcurrentDictionaryAddOrGet(メンバーはすでにアンダースコアによって、そのようにマークされているため、冗長)NBあなたの定数などの繰り返しthis.クラッタ。
Eugene Beresovsky、2015

1
ユージーン、ありがとう。私は「これ」を入れる.NET Reflectorのヘビーユーザーです。すべての非静的フィールド。そのため、私は同じものを好むようになりました。この非並行インターフェースが適切でないことについて:私の実装に対して複数のアクションを実行しようとすると信頼できなくなる可能性があることは間違いなく正しいです。ただし、ここでの要件は、コレクションを破損することなく、単一のアクション(追加、削除、クリア、または列挙)を実行できることだけです。基本的に、すべての周りにロックステートメントを配置する必要がなくなります。
ブライアンブース

11

ConcurrentList(リンクされたリストではなく、サイズ変更可能な配列として)は、非ブロッキング操作で書き込むのは簡単ではありません。そのAPIは、「並行」バージョンにうまく変換できません。


12
書くのが難しいだけでなく、便利なインターフェースを理解するのも難しいです。
CodesInChaos

11

ConcurrentListがない理由は、基本的には書き込めないためです。その理由は、IListのいくつかの重要な操作がインデックスに依存しており、単純な方法では機能しないためです。例えば:

int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");

著者が追っている効果は、「cat」の前に「dog」を挿入することですが、マルチスレッド環境では、これらの2行のコードの間のリストに何が起きてもかまいません。たとえば、別のスレッドがを実行しlist.RemoveAt(0)、リスト全体を左にシフトする場合がありますが、決定的に、catIndexは変更されません。ここでの影響は、Insert操作が実際に「犬」猫の前ではなくに置くことです。

この質問に対する「回答」として提供されていると思われるいくつかの実装は意味がありますが、上記のように、信頼できる結果を提供していません。マルチスレッド環境で本当にリストのようなセマンティクスが必要な場合は、内部にロックを設定してもそこに到達できません、リスト実装メソッドて。使用するすべてのインデックスが、ロックのコンテキスト内に完全に存在することを確認する必要があります。結論としては、正しいロックのあるマルチスレッド環境でリストを使用できますが、リスト自体をその世界に存在させることはできません。

並行リストが必要だと思う場合、実際には2つの可能性があります。

  1. 本当に必要なのはConcurrentBagです
  2. 独自のコレクションを作成する必要があります。おそらく、リストと独自の同時実行制御を使用して実装します。

ConcurrentBagがあり、それをIListとして渡す必要がある位置にいる場合は、問題があります。呼び出しているメソッドが、猫&で上記のようなことをしようとすると指定しているためです。犬。ほとんどの世界でそれが意味することは、呼び出すメソッドがマルチスレッド環境で機能するように構築されていないということです。つまり、リファクタリングするようにリファクタリングするか、できない場合は非常に慎重に処理する必要があります。ほぼ確実に、独自のロックを使用して独自のコレクションを作成し、ロック内で問題のメソッドを呼び出す必要があります。


5

例では、書き込み上回って大幅に読み込み、または(しかし頻繁)の書き込みは非並行しているコピー・オン・ライトアプローチが適切であり得ます。

以下に示す実装は

  • ロックレス
  • 同時読み取りが非常に高速で、同時変更が進行中であっても-どれだけ時間がかかっても
  • 「スナップショット」は不変であるため、ロックなしの原子性が可能です。つまり、var snap = _list; snap[snap.Count - 1];(もちろん、空のリストを除いて)スローされず、スナップショットセマンティクスを使用してスレッドセーフな列挙が無料で得られます。
  • 一般的実装されあらゆるデータ構造およびあらゆるタイプの変更に適用可能
  • シンプルな、つまりコードの読み取りによるテスト、デバッグ、検証が簡単
  • .Net 3.5で使用可能

コピーオンライトを機能させるには、データ構造を実質的に不変に保つ必要があります。つまり、他のスレッドで使用できるようにした後で、データ構造を変更することはできません。変更したいときは、

  1. 構造を複製する
  2. クローンに変更を加える
  3. 変更されたクローンへの参照をアトミックにスワップします

コード

static class CopyOnWriteSwapper
{
    public static void Swap<T>(ref T obj, Func<T, T> cloner, Action<T> op)
        where T : class
    {
        while (true)
        {
            var objBefore = Volatile.Read(ref obj);
            var newObj = cloner(objBefore);
            op(newObj);
            if (Interlocked.CompareExchange(ref obj, newObj, objBefore) == objBefore)
                return;
        }
    }
}

使用法

CopyOnWriteSwapper.Swap(ref _myList,
    orig => new List<string>(orig),
    clone => clone.Add("asdf"));

より高いパフォーマンスが必要な場合は、メソッドを非活性化するのに役立ちます。たとえば、必要な変更の種類(追加、削除など)ごとに1つのメソッドを作成し、関数ポインターclonerとをハードコードしますop

注意#1誰も(おそらく)不変のデータ構造を変更しないようにするのはあなたの責任です。これを防ぐために一般的な実装でできることは何もありませんが、に特化するとList<T>List.AsReadOnly()を使用して変更を防ぐことができます。

注意#2リストの値に注意してください。上記のコピーオンライトのアプローチは、それらのリストメンバーシップのみを保護しますが、文字列ではなく、他の変更可能なオブジェクトをそこに配置する場合、スレッドの安全性(ロックなど)に注意する必要があります。しかし、これはこのソリューションに直交しており、たとえば変更可能な値のロックは問題なく簡単に使用できます。あなたはそれを意識する必要があるだけです。

注意#3データ構造が巨大で頻繁に変更する場合、書き込みすべてコピーのアプローチは、メモリの消費量と、関連するコピーのCPUコストの両方の点で、法外になる場合があります。その場合は、代わりにMSの不変コレクションを使用することをお勧めします。


3

System.Collections.Generic.List<t>複数のリーダーに対してすでにスレッドセーフです。複数のライターがスレッドセーフにしようとしても意味がありません。(理由のためにヘンクとスティーブンはすでに述べた)


5つのスレッドをリストに追加するシナリオを見ることができませんか?このようにして、すべてが終了する前であっても、リストにレコードが蓄積されているのを確認できます。
アラン・

9
@Alan-ConcurrentQueue、ConcurrentStack、またはConcurrentBagになります。ConcurrentListを理解するには、使用可能なクラスでは不十分なユースケースを提供する必要があります。インデックスの要素が同時削除によってランダムに変化する可能性があるのに、なぜインデックス付きアクセスが必要なのかわかりません。また、「ロックされた」読み取りの場合、既存の並行クラスのスナップショットを取得して、それらをリストに入れることができます。
Zarat

そのとおりです-インデックス付きアクセスは必要ありません。私は通常、IList <T>を、新しい要素を.Add(T)できるIEnumerableのプロキシとして使用します。それが問題の出所です。
2011

@Alan:次に、リストではなくキューが必要です。
Billy ONeal

3
私はあなたが間違っていると思います。言います:複数の読者にとって安全であるということは、同時に書くことができないということではありません。書き込みも削除を意味し、反復中に削除するとエラーが発生します。
Eric Ouellet、2011

2

一部の人々はいくつかの商品のポイント(そして私の考えのいくつか)を強調しました:

  • ランダムアクセサー(インデクサー)を使用できないのとは思えないかもしれませんが、私にはそれは問題なく見えます。マルチスレッドコレクションには、インデクサーや削除のように失敗する可能性のある多くのメソッドがあると考えるだけです。「fail」や単に「end at end」のような書き込みアクセサーの失敗(フォールバック)アクションを定義することもできます。
  • マルチスレッドのコンテキストで常に使用されるのは、マルチスレッドのコレクションだからではありません。または、1人のライターと1人のリーダーだけが使用することもできます。
  • インデクサーを安全に使用できるもう1つの方法は、ルートを使用してコレクションのロックにアクションをラップすることです(公開されている場合)。
  • 多くの人にとって、rootLockを可視化することは、再度「グッドプラクティス」になります。これが隠されていると、ユーザーの柔軟性が大幅に低下するため、この点については100%確信が持てません。私たちは常にマルチスレッドのプログラミングは誰のためでもないことを覚えておく必要があります。あらゆる不正使用を防ぐことはできません。
  • Microsoftは、マルチスレッドコレクションを適切に使用するために、いくつかの作業を行い、新しい標準を定義する必要があります。最初に、IEnumeratorにはmoveNextを含めないでください。ただし、trueまたはfalseを返し、T型の出力パラメーターを取得するGetNextを含める必要があります(この方法では、反復処理がブロックされなくなります)。また、Microsoftは既にforeachで内部的に "using"を使用していますが、 "using"でラップせずに直接IEnumeratorを使用する場合もあります(コレクションビューのバグであり、おそらくより多くの場所にあります)。このバグは、安全なイテレータの潜在的な可能性を排除します...コンストラクタでコレクションをロックし、Disposeメソッドでロックを解除するイテレータ-foreachメソッドをブロックします。

それは答えではありません。これは、特定の場所に実際には適合しないコメントのみです。

...私の結論では、Microsoftは、マルチスレッドコレクションを使いやすくするために、「foreach」にいくつかの深い変更を加える必要があります。また、IEnumeratorの使用に関する独自のルールに従う必要があります。それまでは、ブロッキングイテレータを使用するMultiThreadListを簡単に作成できますが、「IList」には従いません。代わりに、例外なく「挿入」、「削除」、およびランダムアクセサー(インデクサー)で失敗する可能性のある独自の「IListPersonnal」インターフェイスを定義する必要があります。しかし、それが標準的でないなら誰がそれを使いたいのでしょうか?


ConcurrentOrderedBag<T>読み取り専用の実装を含むを簡単に作成できIList<T>ますが、完全にスレッドセーフなint Add(T value)メソッドも提供されます。なぜForEach変更が必要になるのかわかりません。Microsoftは明示的にそう述べていませんが、彼らの慣行IEnumerator<T>では、作成時に存在していたコレクションコンテンツを列挙することは完全に許容できると示唆しています。コレクションが変更された例外は、列挙子がグリッチのない操作を保証できない場合にのみ必要です。
スーパーキャット2013

MTコレクションを反復処理すると、デザインの方法によって、例外が発生する可能性があります。すべての例外をトラップしますか?私自身の本では、例外は例外であり、通常のコード実行では発生しません。それ以外の場合は、例外を防ぐために、コレクションをロックするか、コピーを取得(安全な方法でロック)するか、コレクションに非常に複雑なメカニズムを実装して、同時実行による例外の発生を防ぐ必要があります。私が思ったのは、それぞれが発生する間、コレクションをロックし、関連するコードを追加するIEnumeratorMTを追加すると便利だということでした
Eric Ouellet

発生する可能性のあるもう1つのことは、イテレーターを取得したときにコレクションをロックでき、イテレーターがGC収集されたときにコレクションのロックを解除できることです。Microsfotによると、彼らはすでにIEnumerableがIDisposableでもあるかどうかを確認し、そうである場合はForEachの最後でGCを呼び出します。主な問題は、GCを呼び出さずに他の場所でIEnumerableも使用することです。その場合、これに依存することはできません。IEnumerableの有効化ロック用の新しい明確なMTインターフェイスがあれば、問題の少なくとも一部は解決できます。(それを人々がそれを呼ばないことを妨げないでしょう)。
Eric Ouellet、

パブリックGetEnumeratorメソッドがコレクションを返した後、ロックしたままにするのは非常に悪い方法です。このような設計は、簡単にデッドロックを引き起こす可能性があります。IEnumerable<T>列挙は、コレクションが変更されても完了することが期待できるかどうかの兆候を提供しません。最善の方法は、独自のメソッドを記述して独自のメソッドを記述しIEnumerable<T>IEnumerable<T>がスレッドセーフな列挙をサポートしている場合にのみスレッドセーフになるという事実をドキュメントに受け入れるメソッドを用意することです。
スーパーキャット2013

IEnumerable<T>戻り値の型を持つ「スナップショット」メソッドが含まれていれば、最も役立つはずIEnumerable<T>です。不変のコレクションは自分自身を返すことができます。他に何もそれ自体をList<T>またはにコピーしてそれをT[]呼び出さなければ、境界のあるコレクションができGetEnumeratorます。一部の無制限のコレクションはを実装できますがSnapshot、そうでないものは、リストにその内容を埋めようとしないと例外をスローできません。
スーパーキャット2013

1

逐次実行コードでは、使用されるデータ構造は、(よく書かれた)同時実行コードとは異なります。その理由は、順次コードは暗黙的な順序を意味するためです。ただし、並行コードは順序を意味しません。さらに良いのは、定義された順序がないことを意味します!

このため、(Listなどの)暗黙の順序を持​​つデータ構造は、同時発生する問題の解決にはあまり役立ちません。リストは順序を意味しますが、その順序が明確に定義されていません。このため、リストを操作するコードの実行順序は、リストの暗黙的な順序を(ある程度)決定します。これは、効率的な並行ソリューションと直接競合します。

並行性はデータの問題であり、コードの問題ではないことに注意してください。最初にコードを実装して(または既存の順次コードを書き換えて)、うまく設計された並行ソリューションを取得することはできません。暗黙的な順序付けは並行システムには存在しないことを念頭に置いて、最初にデータ構造を設計する必要があります。


1

ロックレスのコピーと書き込みのアプローチは、あまりにも多くのアイテムを扱っていない場合に最適です。ここに私が書いたクラスがあります:

public class CopyAndWriteList<T>
{
    public static List<T> Clear(List<T> list)
    {
        var a = new List<T>(list);
        a.Clear();
        return a;
    }

    public static List<T> Add(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Add(item);
        return a;
    }

    public static List<T> RemoveAt(List<T> list, int index)
    {
        var a = new List<T>(list);
        a.RemoveAt(index);
        return a;
    }

    public static List<T> Remove(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Remove(item);
        return a;
    }

}

使用例:Orders_BUY = CopyAndWriteList.Clear(orders_BUY);


ロックする代わりに、リストのコピーを作成し、リストを変更して、新しいリストへの参照を設定します。したがって、反復している他のスレッドは問題を引き起こしません。
Rob The Quant

0

私はブライアンのようなものを実装しました。鉱山は異なります:

  • アレイは直接管理しています。
  • tryブロック内でロックを入力しません。
  • yield return列挙子の作成に使用します。
  • ロックの再帰をサポートします。これにより、反復中にリストから読み取ることができます。
  • 可能な場合は、アップグレード可能な読み取りロックを使用しています。
  • DoSyncそしてGetSync、リストへの排他的アクセスを必要とする順次的な相互作用を可能にするメソッド。

コード

public class ConcurrentList<T> : IList<T>, IDisposable
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private int _count = 0;

    public int Count
    {
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _count;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public int InternalArrayLength
    { 
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _arr.Length;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    private T[] _arr;

    public ConcurrentList(int initialCapacity)
    {
        _arr = new T[initialCapacity];
    }

    public ConcurrentList():this(4)
    { }

    public ConcurrentList(IEnumerable<T> items)
    {
        _arr = items.ToArray();
        _count = _arr.Length;
    }

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {       
            var newCount = _count + 1;          
            EnsureCapacity(newCount);           
            _arr[_count] = item;
            _count = newCount;                  
        }
        finally
        {
            _lock.ExitWriteLock();
        }       
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null)
            throw new ArgumentNullException("items");

        _lock.EnterWriteLock();

        try
        {           
            var arr = items as T[] ?? items.ToArray();          
            var newCount = _count + arr.Length;
            EnsureCapacity(newCount);           
            Array.Copy(arr, 0, _arr, _count, arr.Length);       
            _count = newCount;
        }
        finally
        {
            _lock.ExitWriteLock();          
        }
    }

    private void EnsureCapacity(int capacity)
    {   
        if (_arr.Length >= capacity)
            return;

        int doubled;
        checked
        {
            try
            {           
                doubled = _arr.Length * 2;
            }
            catch (OverflowException)
            {
                doubled = int.MaxValue;
            }
        }

        var newLength = Math.Max(doubled, capacity);            
        Array.Resize(ref _arr, newLength);
    }

    public bool Remove(T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {           
            var i = IndexOfInternal(item);

            if (i == -1)
                return false;

            _lock.EnterWriteLock();
            try
            {   
                RemoveAtInternal(i);
                return true;
            }
            finally
            {               
                _lock.ExitWriteLock();
            }
        }
        finally
        {           
            _lock.ExitUpgradeableReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterReadLock();

        try
        {    
            for (int i = 0; i < _count; i++)
                // deadlocking potential mitigated by lock recursion enforcement
                yield return _arr[i]; 
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private int IndexOfInternal(T item)
    {
        return Array.FindIndex(_arr, 0, _count, x => x.Equals(item));
    }

    public void Insert(int index, T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {                       
            if (index > _count)
                throw new ArgumentOutOfRangeException("index"); 

            _lock.EnterWriteLock();
            try
            {       
                var newCount = _count + 1;
                EnsureCapacity(newCount);

                // shift everything right by one, starting at index
                Array.Copy(_arr, index, _arr, index + 1, _count - index);

                // insert
                _arr[index] = item;     
                _count = newCount;
            }
            finally
            {           
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }


    }

    public void RemoveAt(int index)
    {   
        _lock.EnterUpgradeableReadLock();
        try
        {   
            if (index >= _count)
                throw new ArgumentOutOfRangeException("index");

            _lock.EnterWriteLock();
            try
            {           
                RemoveAtInternal(index);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }
    }

    private void RemoveAtInternal(int index)
    {           
        Array.Copy(_arr, index + 1, _arr, index, _count - index-1);
        _count--;

        // release last element
        Array.Clear(_arr, _count, 1);
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {        
            Array.Clear(_arr, 0, _count);
            _count = 0;
        }
        finally
        {           
            _lock.ExitWriteLock();
        }   
    }

    public bool Contains(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item) != -1;
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {       
        _lock.EnterReadLock();
        try
        {           
            if(_count > array.Length - arrayIndex)
                throw new ArgumentException("Destination array was not long enough.");

            Array.Copy(_arr, 0, array, arrayIndex, _count);
        }
        finally
        {
            _lock.ExitReadLock();           
        }
    }

    public bool IsReadOnly
    {   
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {           
                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                return _arr[index]; 
            }
            finally
            {
                _lock.ExitReadLock();               
            }           
        }
        set
        {
            _lock.EnterUpgradeableReadLock();
            try
            {

                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                _lock.EnterWriteLock();
                try
                {                       
                    _arr[index] = value;
                }
                finally
                {
                    _lock.ExitWriteLock();              
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

        }
    }

    public void DoSync(Action<ConcurrentList<T>> action)
    {
        GetSync(l =>
        {
            action(l);
            return 0;
        });
    }

    public TResult GetSync<TResult>(Func<ConcurrentList<T>,TResult> func)
    {
        _lock.EnterWriteLock();
        try
        {           
            return func(this);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {   
        _lock.Dispose();
    }
}

2つのスレッドがtryブロックの先頭Removeまたはインデクサーセッターに同時に入るとどうなりますか?
ジェームズ

不可能と思われる@ジェームズ。msdn.microsoft.com/en-us/library/…解説をお読みください。このコードを実行すると、2回目にそのロックを入力することはありません。gist.github.com
Ronnie Overby

@Ronny Overby:興味深い。それを考えると、アップグレード可能な読み取りロックと書き込みロックの間の時間に実行された唯一の操作であるすべての関数からUpgradableReadLockを削除した場合、これははるかに優れたパフォーマンスになると思います。パラメータが範囲外かどうかを確認するチェックよりも、書き込みロック内でそのチェックを実行するだけでパフォーマンスが向上する可能性が高くなります。
ジェームズ

このクラスもあまり有用ではないように思われます。オフセットベースの関数(そのほとんど)は、外部ロックスキームがない限り、安全に使用することができないためです。あなたが実際にそれを得たときから、何かを得る。
James

1
IList並行シナリオでのセマンティクスの有用性はせいぜい限られていることを認識しているとして、私は記録に残りたかったのです。このコードを書いたのは、おそらくその実現に至る前です。私の経験は、受け入れられた回答の作成者と同じです。同期とIList <T>について知っていることを試してみて、それを行うことで何かを学びました。
ロニーオーバーバイ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.