我很高兴看到System.Collections.Concurrent
.Net 4.0中的新名称空间,这真是太好了!我见过ConcurrentDictionary
,ConcurrentQueue
,ConcurrentStack
,ConcurrentBag
和BlockingCollection
。
似乎神秘失踪的一件事是ConcurrentList<T>
。我是否必须自己写一个(或从网络上获取它:))?
我在这里错过明显的东西吗?
我很高兴看到System.Collections.Concurrent
.Net 4.0中的新名称空间,这真是太好了!我见过ConcurrentDictionary
,ConcurrentQueue
,ConcurrentStack
,ConcurrentBag
和BlockingCollection
。
似乎神秘失踪的一件事是ConcurrentList<T>
。我是否必须自己写一个(或从网络上获取它:))?
我在这里错过明显的东西吗?
Answers:
我试了一下(也:在GitHub上)。我的实现存在一些问题,这里不再赘述。让我告诉您,更重要的是,我学到了什么。
首先,您将无法获得IList<T>
无锁且线程安全的完整实现。特别是,除非您还忘记了O(1)随机访问(即除非您“作弊”并仅使用某种类型的链表并让索引很烂),否则随机插入和删除将无法工作。
我想可能是值得的是线程安全的,集有限的IList<T>
:尤其是,一个将允许Add
并提供随机只读通过索引访问(但没有Insert
,RemoveAt
等,还没有随机写入访问)。
这是我ConcurrentList<T>
实施的目标。但是,当我在多线程方案中测试其性能时,我发现仅同步添加到a List<T>
会更快。基本上,添加到a List<T>
已经快如闪电;所涉及的计算步骤的复杂性很小(增加索引并分配给数组中的元素;的确如此)。您将需要进行大量并发写入,以查看与此相关的任何类型的锁争用。即使在这种情况下,每次写入的平均性能也仍然会击败更昂贵的方法,尽管在中是无锁的ConcurrentList<T>
。
在相对罕见的情况下,列表的内部数组需要调整自身大小,因此您付出的代价很小。因此,最终我得出结论,这是一个只添加ConcurrentList<T>
集合类型就有意义的特殊情况:当您希望保证在每次调用中添加元素的开销很低时(因此,与摊销的性能目标相对)。
它根本不像您想的那样有用。
List<T>
使用基于监视器的旧式同步的类似功能,SynchronizedCollection<T>
则BCL中隐藏了以下内容:msdn.microsoft.com/en-us/library/ms668265.aspx
ConcurrentList
要赢得胜利的最大场景是,当没有很多活动添加到列表中,但是有许多并发读者时。可以将读者的开销减少到一个内存屏障(甚至可以消除读者不关心稍微陈旧的数据的情况)。
ConcurrentList<T>
以这样一种方式构造一个琐碎的事情:确保读者可以看到一致的状态而无需任何锁定,而且开销相对较小。当列表从例如大小32扩展到64时,保留size-32数组并创建一个新的size-64数组。在添加下一个32个项目时,将其放入新阵列的32-63插槽中,然后将旧项目从size-32阵列复制到新阵列中。在添加第64个项目之前,读者将在size-32数组中查找项目0-31,并在size-64数组中查找项目32-63。
您将使用ConcurrentList做什么?
在线程世界中,“随机访问”容器的概念没有看起来那么有用。该声明
if (i < MyConcurrentList.Count)
x = MyConcurrentList[i];
总体而言,它仍然不是线程安全的。
与其创建ConcurrentList,不如尝试使用现有内容构建解决方案。最常见的类是ConcurrentBag,尤其是BlockingCollection。
Monitor
它了,那么就没有理由使用并发列表了。
充分考虑到已经提供的好答案,有时候我只是想要一个线程安全的IList。没有进步或幻想。在许多情况下,性能都很重要,但有时并不在意。是的,如果没有“ TryGetValue”等方法,总会有挑战,但是大多数情况下,我只想列举一些我可以列举的东西,而不必担心在所有事物上加锁。是的,有人可能会在我的实现中发现一些“错误”,可能会导致死锁或某些事情(我想),但是老实说:在多线程方面,如果您编写不正确的代码,无论如何都会陷入僵局。考虑到这一点,我决定制作一个可满足这些基本需求的简单ConcurrentList实现。
而其价值:我做了一个基本测试,将10,000,000个项目添加到常规List和ConcurrentList中,结果是:
列表完成时间: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
}
RemoveAt(int index)
是: 永远不是线程安全的,Insert(int index, T item)
仅对于index == 0是安全的,的返回IndexOf()
立即过时等。甚至不要以开头this[int]
。
var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";
。通常,任何读写组合在同时执行时都会导致您陷入严重麻烦。这就是为什么并发数据结构通常为诸如此类ConcurrentDictionary
的数据提供方法的原因AddOrGet
。NB您的常量(并且冗余,因为成员已经被下划线标记了)重复的this.
混乱。
ConcurrentList
(作为可调整大小的数组,而不是链表),使用非阻塞操作不容易编写。它的API不能很好地转换为“并发”版本。
之所以没有ConcurrentList是因为从根本上来说无法编写它。原因是IList中的几个重要操作都依赖于索引,而普通操作不起作用。例如:
int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");
作者追求的效果是在“ cat”之前插入“ dog”,但是在多线程环境中,这两行代码之间的列表可能会发生任何事情。例如,另一个线程可以这样做list.RemoveAt(0)
,将整个列表向左移动,但是至关重要的是,catIndex不会改变。这里的影响是该Insert
操作实际上会将“狗” 放在猫的后面,而不是在猫的前面。
您看到的作为该问题的“答案”的几种实现方式是很好的,但是如上所示,它们并不能提供可靠的结果。如果您确实希望在多线程环境中使用类似列表的语义,则无法通过在其中放入锁来达到目的。在列表实现方法中此目的。您必须确保使用的任何索引完全位于锁的上下文中。结果是您可以在具有正确锁定的多线程环境中使用列表,但是列表本身不能存在于该世界中。
如果您认为需要并发列表,则实际上只有两种可能性:
如果您有一个ConcurrentBag,并且处于需要将其作为IList传递的位置,那么您就遇到了问题,因为您所调用的方法已指定他们可能会尝试像上面用cat&做的事情狗。在大多数情况下,这意味着要调用的方法根本无法在多线程环境中工作。这意味着您要么重构它,要么重构它,否则,您将不得不非常小心地处理它。您几乎肯定会需要使用自己的锁创建自己的集合,并在锁内调用有问题的方法。
如果读取的数量大大超过写入的数量,或者(无论多么频繁)写入是非并发的,则写时复制方法可能是合适的。
下面显示的实现是
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"));
如果您需要更高的性能,将有助于使该方法无效化,例如,为所需的每种修改类型(添加,删除,...)创建一个方法,并对功能指针cloner
和进行硬编码op
。
NB#1您有责任确保没有人修改(据说)不可变的数据结构。我们无法在通用实现中采取任何措施来防止这种情况的发生,但是当专门针对时List<T>
,您可以防止使用List.AsReadOnly()进行修改。
注意#2请注意列表中的值。上面的“写时复制”方法仅保护其列表成员身份,但是如果您不放置字符串,而是放置其他可变对象,则必须注意线程安全(例如,锁定)。但这与该解决方案正交,例如可以轻松使用可变值的锁定而不会出现问题。您只需要意识到这一点。
NB#3如果您的数据结构庞大且需要经常修改,那么在内存消耗和所涉及的复制的CPU成本方面,写时复制全部方法可能会被禁止。在这种情况下,您可能想使用MS的不可变集合。
System.Collections.Generic.List<t>
对于多个读者来说已经是线程安全的。试图使它对于多个编写者来说都是线程安全的,这没有任何意义。(由于亨克和斯蒂芬已经提到的原因)
有些人对某些商品点(和我的一些想法)感到高兴:
那不是答案。这只是真正不适合特定位置的注释。
...我的结论是,Microsoft必须对“ foreach”进行一些深刻的更改,以使MultiThreaded集合更易于使用。它还必须遵循IEnumerator的使用规则。在此之前,我们可以轻松编写一个MultiThreadList,它使用阻塞迭代器,但不会遵循“ IList”。取而代之的是,您将必须定义自己的“ IListPersonnal”接口,该接口在“插入”,“删除”和随机访问器(索引器)上毫无例外可能会失败。但是,如果它不是标准的,谁愿意使用它呢?
ConcurrentOrderedBag<T>
包含的只读实现的IList<T>
,但也可以提供一种完全线程安全的int Add(T value)
方法。我不明白为什么ForEach
需要任何更改。尽管Microsoft并未明确表示,但实践表明,IEnumerator<T>
枚举创建时存在的集合内容是完全可以接受的。仅当枚举器无法保证无故障操作时,才需要使用collection-modified异常。
GetEnumerator
方法在集合返回后将其锁定,这是非常糟糕的形式。这样的设计很容易导致死锁。不能IEnumerable<T>
提供即使修改了集合也无法期望枚举的迹象;最好的办法是编写自己的方法,这样他们就可以这样做,并且拥有接受IEnumerable<T>
文档的方法,即只有IEnumerable<T>
支持线程安全枚举才可以保证线程安全。
IEnumerable<T>
包含带有return type的“ Snapshot”方法,那将是最有帮助的IEnumerable<T>
。不可变的收藏可以返回自己;如果没有其他限制,有界集合可以将自身复制到List<T>
或T[]
并对其进行调用GetEnumerator
。可以实现一些无界的集合Snapshot
,而那些不尝试不填充其内容的列表就无法引发异常的集合。
如果您不处理太多项目,则无锁“复制和写入”方法非常有用。这是我写的一堂课:
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);
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();
}
}
try
块的开头Remove
或索引器设置器,会发生什么情况?
IList
语义在并发场景中的用处最多是有限的。我可能在实现之前就编写了这段代码。我的经验与接受答案的作者相同:我尝试了一下有关同步和IList <T>的知识,并从中学到了一些东西。