.Net 4.0の新しいSystem.Collections.Concurrent
名前空間を見て、とても嬉しかったです! ConcurrentDictionary
、ConcurrentQueue
、ConcurrentStack
、ConcurrentBag
、BlockingCollection
を見てきました。
不思議なことに欠けていると思われるものの1つはConcurrentList<T>
です。自分でそれを書く必要がありますか(または、Webから取得する必要があります:))?
ここで明らかな何かを見逃していますか?
I しばらく試してみた (また: GitHubで )。私の実装にはいくつかの問題がありましたが、ここでは説明しません。さらに重要なことは、私が学んだことをお話ししましょう。
まず、ロックレスでスレッドセーフなIList<T>
の完全な実装を取得する方法はありません。特に、O(1)ランダムアクセスも忘れない限り、ランダムな挿入と削除はnot動作します(つまり、「チート」して何らかのソートを使用しない限り)リンクされたリストとインデックス作成を吸わせてください)。
私がthoughtに値するのは、スレッドセーフで限定されたIList<T>
のサブセットでした。特に、Add
を許可し、ランダムなread-onlyインデックスによるアクセスを提供します(ただし、Insert
、RemoveAt
などはなく、ランダムwriteアクセスもありません)。
これが 私のConcurrentList<T>
実装 の目標でした。しかし、マルチスレッドシナリオでパフォーマンスをテストしたとき、List<T>
へのaddの同期が単純であることがわかりました。基本的に、List<T>
への追加はすでに非常に高速です。関連する計算ステップの複雑さはごくわずかです(インデックスをインクリメントし、配列内の要素に割り当てます。これは実際にです)。これに関するロック競合を確認するには、同時書き込みのtonが必要です。そして、それでも、各書き込みの平均パフォーマンスは、ConcurrentList<T>
のロックレス実装にもかかわらず、より高価なものを凌ぐでしょう。
リストの内部配列のサイズを変更する必要があるという比較的まれなイベントでは、少額の費用がかかります。最終的に、これはoneニッチシナリオであり、追加のみのConcurrentList<T>
コレクションタイプが理にかなっていると結論付けました。必要な場合guaranteed要素を追加するオーバーヘッドが低い- 1回の呼び出しごと(したがって、償却されたパフォーマンス目標とは対照的に)。
クラスは、あなたが考えるほど有用ではありません。
ConcurrentListを何に使用しますか?
スレッド化された世界でのランダムアクセスコンテナーの概念は、表示されるほど有用ではありません。声明
if (i < MyConcurrentList.Count)
x = MyConcurrentList[i];
全体としてはまだスレッドセーフではありません。
ConcurrentListを作成する代わりに、そこにあるものでソリューションを構築してみてください。最も一般的なクラスは、ConcurrentBag、特にBlockingCollectionです。
すでに提供されたすばらしい回答にすべて敬意を払って、スレッドセーフな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
}
ConcurrentList
(リンクされたリストではなく、サイズ変更可能な配列として)は、ノンブロッキング操作で書くのは簡単ではありません。そのAPIは、「同時」バージョンにうまく変換されません。
ConcurrentListがない理由は、ConcurrentListが基本的に作成できないためです。理由は、IListのいくつかの重要な操作がインデックスに依存しており、単なるプレーンが機能しないためです。例えば:
int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");
著者が追求している効果は、「猫」の前に「犬」を挿入することですが、マルチスレッド環境では、これらの2行のコードの間でリストに何かが起こります。たとえば、別のスレッドがlist.RemoveAt(0)
を実行し、リスト全体を左にシフトしますが、決定的に、catIndexは変更されません。ここでの影響は、Insert
操作が実際に「犬」を置くということですafter catの前ではなく。
この質問に対する「答え」として提供されると思われるいくつかの実装は意味がありますが、上記のように、信頼できる結果を提供しません。マルチスレッド環境でリストのようなセマンティクスが本当に必要な場合は、ロックを入れてもそこに到達できませんinsideリスト実装メソッド。使用するインデックスがロックのコンテキスト内に完全に収まるようにする必要があります。結果として、適切なロックを備えたマルチスレッド環境でリストを使用できますが、リスト自体をその世界に存在させることはできません。
並行リストが必要だと思う場合、実際には2つの可能性があります。
ConcurrentBagがあり、それをIListとして渡す必要がある位置にいる場合は、問題が発生します。呼び出すメソッドが、上記のcat&犬。ほとんどの世界では、それが意味することは、呼び出しているメソッドがマルチスレッド環境で動作するように構築されていないことです。つまり、リファクタリングすることで、リファクタリングすることができます。できない場合は、慎重に処理する必要があります。ほぼ確実に、独自のロックを使用して独自のコレクションを作成し、ロック内で問題のメソッドを呼び出す必要があります。
読み取りが書き込みを大幅に上回る場合、または(ただし頻繁な)書き込みが非並行の場合、copy-on -writeアプローチが適切な場合があります。
以下に示す実装は
var snap = _list; snap[snap.Count - 1];
は決して(もちろん、空のリストを除いて)スローされませんスナップショットセマンティクスを使用してスレッドセーフな列挙を無料で取得できます。コピーオンライトを機能させるには、データ構造を効果的に不変に保つ必要があります。つまり、他のスレッドで使用可能にした後は、誰も変更できません。変更したいときは、
コード
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 を使用できます。
System.Collections.Generic.List<t>
は、すでに複数のリーダーに対してスレッドセーフです。複数のライターに対してスレッドセーフにすることは意味がありません。 (ヘンクとスティーブンがすでに言及した理由のため)
一部の人々はいくつかの商品ポイント(および私の考えのいくつか)を強調しました:
それは答えではありません。これは、特定の場所に実際には当てはまらないコメントのみです。
...私の結論として、Microsoftは、マルチスレッドコレクションを使いやすくするために、「foreach」にいくつかの深い変更を加える必要があります。また、IEnumeratorの使用に関する独自のルールに従う必要があります。それまでは、ブロッキングイテレータを使用するが、「IList」には従わないMultiThreadListを簡単に作成できます。代わりに、「挿入」、「削除」、およびランダムアクセサー(インデクサー)で例外なく失敗する可能性がある独自の「IListPersonnal」インターフェイスを定義する必要があります。しかし、標準ではない場合、誰がそれを使用したいでしょうか?
ロックレスコピーアンドライトアプローチは、あまりにも多くのアイテムを処理していない場合に最適です。これが私が書いたクラスです:
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);
コードを連続して実行する場合、使用されるデータ構造は、同時に実行されるコードとは異なります(十分に記述されています)。その理由は、シーケンシャルコードが暗黙的な順序を暗示しているためです。ただし、並行コードは順序を意味するものではありません。さらに良いのは、定義された順序がないことを意味します!
このため、暗黙の順序(リストなど)のデータ構造は、並行問題の解決にはあまり役立ちません。リストは順序を意味しますが、その順序が明確に定義されていません。このため、リストを操作するコードの実行順序により、リストの暗黙的な順序が(ある程度)決定されます。これは、効率的な同時ソリューションと直接競合します。
同時実行はコードの問題ではなく、データの問題であることを忘れないでください!最初にコードを実装する(または既存のシーケンシャルコードを書き換える)ことはできず、適切に設計された同時ソリューションを取得することはできません。並行システムには暗黙的な順序付けが存在しないことに留意しながら、最初にデータ構造を設計する必要があります。
Brian's に似たものを実装しました。私のは違う:
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();
}
}