web-dev-qa-db-ja.com

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

.Net 4.0の新しいSystem.Collections.Concurrent名前空間を見て、とても嬉しかったです! ConcurrentDictionaryConcurrentQueueConcurrentStackConcurrentBagBlockingCollectionを見てきました。

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

ここで明らかな何かを見逃していますか?

187
Alan

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

まず、ロックレスでスレッドセーフなIList<T>の完全な実装を取得する方法はありません。特に、O(1)ランダムアクセスも忘れない限り、ランダムな挿入と削除はnot動作します(つまり、「チート」して何らかのソートを使用しない限り)リンクされたリストとインデックス作成を吸わせてください)。

私がthoughtに値するのは、スレッドセーフで限定されたIList<T>のサブセットでした。特に、Addを許可し、ランダムなread-onlyインデックスによるアクセスを提供します(ただし、InsertRemoveAtなどはなく、ランダムwriteアクセスもありません)。

これが 私のConcurrentList<T>実装 の目標でした。しかし、マルチスレッドシナリオでパフォーマンスをテストしたとき、List<T>へのaddの同期が単純であることがわかりました。基本的に、List<T>への追加はすでに非常に高速です。関連する計算ステップの複雑さはごくわずかです(インデックスをインクリメントし、配列内の要素に割り当てます。これは実際にです)。これに関するロック競合を確認するには、同時書き込みのtonが必要です。そして、それでも、各書き込みの平均パフォーマンスは、ConcurrentList<T>のロックレス実装にもかかわらず、より高価なものを凌ぐでしょう。

リストの内部配列のサイズを変更する必要があるという比較的まれなイベントでは、少額の費用がかかります。最終的に、これはoneニッチシナリオであり、追加のみのConcurrentList<T>コレクションタイプが理にかなっていると結論付けました。必要な場合guaranteed要素を追加するオーバーヘッドが低い- 1回の呼び出しごと(したがって、償却されたパフォーマンス目標とは対照的に)。

クラスは、あなたが考えるほど有用ではありません。

159
Dan Tao

ConcurrentListを何に使用しますか?

スレッド化された世界でのランダムアクセスコンテナーの概念は、表示されるほど有用ではありません。声明

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

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

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

34
Henk Holterman

すでに提供されたすばらしい回答にすべて敬意を払って、スレッドセーフな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
}
18
Brian Booth

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

11
Stephen Cleary

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

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

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

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

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

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

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

10
Steve Benz

読み取りが書き込みを大幅に上回る場合、または(ただし頻繁な)書き込みが非並行の場合、copy-on -writeアプローチが適切な場合があります。

以下に示す実装は

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

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

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

N.B。 #3データ構造が巨大で、頻繁に変更する場合、書き込み時のコピーのアプローチは、メモリ消費と関連するコピーのCPUコストの両方の観点から禁止される可能性があります。その場合、代わりにMSの Immutable Collections を使用できます。

5

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

3
Billy ONeal

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

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

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

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

2
Eric Ouellet

ロックレスコピーアンドライトアプローチは、あまりにも多くのアイテムを処理していない場合に最適です。これが私が書いたクラスです:

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);

1
Rob The Quant

コードを連続して実行する場合、使用されるデータ構造は、同時に実行されるコードとは異なります(十分に記述されています)。その理由は、シーケンシャルコードが暗黙的な順序を暗示しているためです。ただし、並行コードは順序を意味するものではありません。さらに良いのは、定義された順序がないことを意味します!

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

同時実行はコードの問題ではなく、データの問題であることを忘れないでください!最初にコードを実装する(または既存のシーケンシャルコードを書き換える)ことはできず、適切に設計された同時ソリューションを取得することはできません。並行システムには暗黙的な順序付けが存在しないことに留意しながら、最初にデータ構造を設計する必要があります。

0
Blueprint41

Brian's に似たものを実装しました。私のは違う:

  • アレイを直接管理します。
  • 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();
    }
}
0
Ronnie Overby