.Net 4.0中没有ConcurrentList <T>?


198

我很高兴看到System.Collections.Concurrent.Net 4.0中的新名称空间,这真是太好了!我见过ConcurrentDictionaryConcurrentQueueConcurrentStackConcurrentBagBlockingCollection

似乎神秘失踪的一件事是ConcurrentList<T>。我是否必须自己写一个(或从网络上获取它:))?

我在这里错过明显的东西吗?



4
@ RodrigoReis,ConcurrentBag <T>是无序集合,而List <T>是有序的。
亚当Calvet Bohl

4
您如何在多线程环境中获得有序集合?通过设计,您将永远无法控制元素的顺序。
杰里米·霍洛瓦奇

使用而不是锁定
埃里克Bergstedt

在dotnet源代码中有一个名为ThreadSafeList.cs的文件,看起来与下面的代码非常相似。它也使用ReaderWriterLockSlim,试图找出为什么用它代替简单的lock(obj)?
colin lamarre

Answers:


166

试了一下(也:在GitHub上)。我的实现存在一些问题,这里不再赘述。让我告诉您,更重要的是,我学到了什么。

首先,您将无法获得IList<T>无锁且线程安全的完整实现。特别是,除非您还忘记了O(1)随机访问(即除非您“作弊”并仅使用某种类型的链表并让索引很烂),否则随机插入和删除将无法工作。

可能是值得的是线程安全的,集有限的IList<T>:尤其是,一个将允许Add并提供随机只读通过索引访问(但没有InsertRemoveAt等,还没有随机写入访问)。

这是ConcurrentList<T>实施的目标。但是,当我在多线程方案中测试其性能时,我发现仅同步添加到a List<T>会更快。基本上,添加到a List<T>已经快如闪电;所涉及的计算步骤的复杂性很小(增加索引并分配给数组中的元素;的确如此)。您将需要进行大量并发写入,以查看与此相关的任何类型的锁争用。即使在这种情况下,每次写入的平均性能也仍然会击败更昂贵的方法,尽管在中是无锁的ConcurrentList<T>

在相对罕见的情况下,列表的内部数组需要调整自身大小,因此您付出的代价很小。因此,最终我得出结论,这是一个只添加ConcurrentList<T>集合类型就有意义的特殊情况:当您希望保证每次调用中添加元素的开销很低时(因此,与摊销的性能目标相对)。

它根本不像您想的那样有用。


52
而且,如果您需要List<T>使用基于监视器的旧式同步的类似功能,SynchronizedCollection<T>则BCL中隐藏了以下内容:msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH 2011年

8
一个小的补充:使用Capacity构造函数参数来避免(尽可能)调整大小方案。
Henk Holterman

2
ConcurrentList要赢得胜利的最大场景是,当没有很多活动添加到列表中,但是有许多并发读者时。可以将读者的开销减少到一个内存屏障(甚至可以消除读者不关心稍微陈旧的数据的情况)。
2013年

2
@Kevin:ConcurrentList<T>以这样一种方式构造一个琐碎的事情:确保读者可以看到一致的状态而无需任何锁定,而且开销相对较小。当列表从例如大小32扩展到64时,保留size-32数组并创建一个新的size-64数组。在添加下一个32个项目时,将其放入新阵列的32-63插槽中,然后将旧项目从size-32阵列复制到新阵列中。在添加第64个项目之前,读者将在size-32数组中查找项目0-31,并在size-64数组中查找项目32-63。
2013年

2
一旦添加了第64个项目,size-32数组仍可用于获取项目0-31,但是读者将不再需要使用它。他们可以对所有项目0-63使用size-64数组,对项目64-127使用size-128数组。选择要使用的两个阵列之一的开销,以及如果需要的话,还需要一个内存屏障,这将小于甚至可以想象得到的最有效的读写器锁的开销。写入可能应该使用锁(可以使用无锁,特别是如果不介意每次插入都创建一个新的对象实例,但是锁应该便宜。)
supercat 2013年

38

您将使用ConcurrentList做什么?

在线程世界中,“随机访问”容器的概念没有看起来那么有用。该声明

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

总体而言,它仍然不是线程安全的。

与其创建ConcurrentList,不如尝试使用现有内容构建解决方案。最常见的类是ConcurrentBag,尤其是BlockingCollection。


好点子。我在做的还是有些平凡。我只是试图将ConcurrentBag <T>分配给IList <T>。我可以将我的属性切换为IEnumerable <T>,但是之后就无法为它添加内容。
艾伦(Alan)

1
@Alan:没有锁定列表是无法实现的。既然您已经可以使用Monitor它了,那么就没有理由使用并发列表了。
Billy ONeal

6
@dcp-是的,这本质上是非线程安全的。ConcurrentDictionary具有在一个原子操作中执行此操作的特殊方法,例如AddOrUpdate,GetOrAdd,TryUpdate等。它们仍然具有ContainsKey,因为有时您只是想知道该键是否在那里而无需修改字典(请考虑HashSet)
Zarat

3
@dcp-ContainsKey本身是线程安全的,您的示例(非ContainsKey!)仅具有竞争条件,因为您根据第一个决定进行了第二次调用,此时可能已经过时。
Zarat 2011年

2
亨克,我不同意。我认为有一个简单的场景可能会非常有用。向其中写入工作线程将UI线程读取并相应地更新接口。如果要以排序方式添加项目,则需要随机访问写入。您还可以使用堆栈和数据视图,但必须维护2个集合:
(。– Eric Ouellet

19

充分考虑到已经提供的好答案,有时候我只是想要一个线程安全的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
}

5
好的,旧的答案却仍然RemoveAt(int index)是: 永远不是线程安全的,Insert(int index, T item)仅对于index == 0是安全的,的返回IndexOf()立即过时等。甚至不要以开头this[int]
汉克·霍尔特曼

2
而且您不需要也不需要〜Finalizer()。
汉克·霍尔特曼

2
您说您已经放弃了防止死锁的可能性,并且ReaderWriterLockSlim可以通过同时使用轻松地使单个死锁EnterUpgradeableReadLock()。但是,您不会使用它,也不会使锁对外界无法访问,也不会例如调用持有读锁的同时输入写锁的方法,因此使用您的类不再会使死锁可能。
尤金·别列索夫斯基

1
非并行接口不适用于并发访问。例如,以下内容不是原子的var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";。通常,任何读写组合在同时执行时都会导致您陷入严重麻烦。这就是为什么并发数据结构通常为诸如此类ConcurrentDictionary的数据提供方法的原因AddOrGet。NB您的常量(并且冗余,因为成员已经被下划线标记了)重复的this.混乱。
尤金·别列索夫斯基

1
谢谢尤金。我是.NET Reflector的重度用户,它放置了“ this”。在所有非静态字段上。因此,我变得越来越喜欢相同的东西。关于此非并行接口不合适:您完全正确的做法是,对我的实现尝试执行多项操作可能会变得不可靠。但是,这里的要求只是简单地执行单个操作(添加,删除,清除,枚举),而不会破坏集合。基本上,不需要在所有内容周围放置锁语句。
布赖恩·布斯

11

ConcurrentList(作为可调整大小的数组,而不是链表),使用非阻塞操作不容易编写。它的API不能很好地转换为“并发”版本。


12
这不仅很难编写,而且甚至很难找出有用的界面。
CodesInChaos 2011年

11

之所以没有ConcurrentList是因为从根本上来说无法编写它。原因是IList中的几个重要操作都依赖于索引,而普通操作不起作用。例如:

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

作者追求的效果是在“ cat”之前插入“ dog”,但是在多线程环境中,这两行代码之间的列表可能会发生任何事情。例如,另一个线程可以这样做list.RemoveAt(0),将整个列表向左移动,但是至关重要的是,catIndex不会改变。这里的影响是该Insert操作实际上会将“狗” 放在猫的后面,而不是在猫的前面。

您看到的作为该问题的“答案”的几种实现方式是很好的,但是如上所示,它们并不能提供可靠的结果。如果您确实希望在多线程环境中使用类似列表的语义,则无法通过在其中放入锁来达到目的。在列表实现方法中此目的。您必须确保使用的任何索引完全位于锁的上下文中。结果是您可以在具有正确锁定的多线程环境中使用列表,但是列表本身不能存在于该世界中。

如果您认为需要并发列表,则实际上只有两种可能性:

  1. 您真正需要的是一个ConcurrentBag
  2. 您需要创建自己的集合,也许使用List和自己的并发控件来实现。

如果您有一个ConcurrentBag,并且处于需要将其作为IList传递的位置,那么您就遇到了问题,因为您所调用的方法已指定他们可能会尝试像上面用cat&做的事情狗。在大多数情况下,这意味着要调用的方法根本无法在多线程环境中工作。这意味着您要么重构它,要么重构它,否则,您将不得不非常小心地处理它。您几乎肯定会需要使用自己的锁创建自己的集合,并在锁内调用有问题的方法。


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

如果您需要更高的性能,将有助于使该方法无效化,例如,为所需的每种修改类型(添加,删除,...)创建一个方法,并对功能指针cloner和进行硬编码op

NB#1您有责任确保没有人修改(据说)不可变的数据结构。我们无法在通用实现中采取任何措施来防止这种情况的发生,但是当专门针对时List<T>,您可以防止使用List.AsReadOnly()进行修改。

注意#2请注意列表中的值。上面的“写时复制”方法仅保护其列表成员身份,但是如果您不放置字符串,而是放置其他可变对象,则必须注意线程安全(例如,锁定)。但这与该解决方案正交,例如可以轻松使用可变值的锁定而不会出现问题。您只需要意识到这一点。

NB#3如果您的数据结构庞大且需要经常修改,那么在内存消耗和所涉及的复制的CPU成本方面,写时复制全部方法可能会被禁止。在这种情况下,您可能想使用MS的不可变集合


3

System.Collections.Generic.List<t>对于多个读者来说已经是线程安全的。试图使它对于多个编写者来说都是线程安全的,这没有任何意义。(由于亨克和斯蒂芬已经提到的原因)


您看不到可能有5个线程添加到列表的情况吗?这样,您甚至可以在列表全部终止之前看到列表累积的记录。
艾伦(Alan)

9
@Alan-可以是ConcurrentQueue,ConcurrentStack或ConcurrentBag。为了理解ConcurrentList,您应该提供一个可用案例不足的用例。我不明白为什么当索引处的元素可以通过并发删除随机更改时为什么要索引访问。对于“锁定”读取,您已经可以对现有并发类进行快照并将它们放在列表中。
Zarat 2011年

您是对的-我不希望索引访问。我通常使用IList <T>作为IEnumerable的代理,可以向其添加.T(T)新元素。确实,这就是问题的出处。
艾伦(Alan)

@Alan:然后您想要一个队列,而不是列表。
Billy ONeal

3
我认为你错了。说:对多个读者来说安全并不意味着您不能同时写作。写操作也意味着删除,并且如果在迭代过程中删除它会收到错误消息。
埃里克·厄勒

2

有些人对某些商品点(和我的一些想法)感到高兴:

  • 对于无法使用随机访问器(索引器)来说,这看起来似乎很疯狂,但对我来说似乎很好。您只需要考虑在多线程集合上有很多可能失败的方法,例如Indexer和Delete。您还可以为写访问器定义失败(失败)操作,例如“失败”或“最后添加”。
  • 并不是因为它是一个多线程集合,所以它将始终在多线程上下文中使用。或者也可以仅由一位作者和一位读者使用。
  • 能够以安全方式使用索引器的另一种方法是使用其根(如果已公开)将动作包装到锁的集合中。
  • 对于许多人来说,使rootLock可见是一种“良好做法”。对于这一点,我不是100%肯定的,因为如果隐藏了它,则会给用户带来很大的灵活性。我们始终必须记住,编程多线程不适合任何人。我们无法防止各种错误的用法。
  • Microsoft将必须做一些工作并定义一些新标准,以引入对Multithreaded集合的正确用法。首先,IEnumerator不应具有moveNext,而应具有返回true或false并获得类型T的out参数的GetNext(这样,迭代将不再受阻)。同样,Microsoft在foreach中已经在内部使用“使用”,但是有时直接使用IEnumerator而不用“使用”包装(集合视图中的错误,并且可能在更多地方使用)-微软建议使用IEnumerator进行包装。此错误为安全的迭代器消除了良好的潜力。迭代器可将构造函数中的集合锁定并对其Dispose方法进行解锁-用于阻塞foreach方法。

那不是答案。这只是真正不适合特定位置的注释。

...我的结论是,Microsoft必须对“ foreach”进行一些深刻的更改,以使MultiThreaded集合更易于使用。它还必须遵循IEnumerator的使用规则。在此之前,我们可以轻松编写一个MultiThreadList,它使用阻塞迭代器,但不会遵循“ IList”。取而代之的是,您将必须定义自己的“ IListPersonnal”接口,该接口在“插入”,“删除”和随机访问器(索引器)上毫无例外可能会失败。但是,如果它不是标准的,谁愿意使用它呢?


可以轻松编写一个ConcurrentOrderedBag<T>包含的只读实现的IList<T>,但也可以提供一种完全线程安全的int Add(T value)方法。我不明白为什么ForEach需要任何更改。尽管Microsoft并未明确表示,但实践表明,IEnumerator<T>枚举创建时存在的集合内容是完全可以接受的。仅当枚举器无法保证无故障操作时,才需要使用collection-modified异常。
supercat 2013年

如您所说,遍历MT集合时,它的设计方式可能会导致异常……我不知道哪个。您会捕获所有异常吗?在我自己的书中,例外是例外,不应在代码的正常执行中发生。否则,为了防止异常,您必须锁定集合或获取副本(以安全的方式即锁定),或者在集合中实现非常复杂的机制以防止由于并发而导致异常。我的想法是,添加一个IEnumeratorMT会很不错,它会在每次出现时锁定集合并添加相关代码...
Eric Ouellet13 2013年

也可能发生的另一件事是,当您获得一个迭代器时,您可以锁定该集合,而当您的迭代器是GC收集时,则可以解锁该集合。根据Microsfot的说法,他们已经检查IEnumerable是否也是IDisposable,如果是,则在ForEach末尾调用GC。主要的问题是,他们在其他地方也使用了IEnumerable而不调用GC,因此您不能依靠它。为IEnumerable启用锁定具有新的清晰MT接口可以解决问题,至少可以解决一部分问题。(这不会阻止人们不称呼它)。
埃里克·厄勒

公共GetEnumerator方法在集合返回后将其锁定,这是非常糟糕的形式。这样的设计很容易导致死锁。不能IEnumerable<T>提供即使修改了集合也无法期望枚举的迹象;最好的办法是编写自己的方法,这样他们就可以这样做,并且拥有接受IEnumerable<T>文档的方法,即只有IEnumerable<T>支持线程安全枚举才可以保证线程安全。
2013年

如果IEnumerable<T>包含带有return type的“ Snapshot”方法,那将是最有帮助的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

我实现了一种与Brian相似的方法。我的是不同的:

  • 我直接管理阵列。
  • 我没有在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();
    }
}

如果两个线程同时进入try块的开头Remove或索引器设置器,会发生什么情况?
詹姆斯

@James看来不可能。阅读msdn.microsoft.com/zh-cn/library/…上的备注。运行此代码,您将永远不会第二次输入该锁:gist.github.com/ronnieoverby/59b715c3676127a113c3
Ronnie Overby

@Ronny Overby:有趣。鉴于此,我怀疑如果从所有函数中删除了UpgradableReadLock会更好地执行所有功能,而在可升级读锁和写锁之间只有一次操作执行此操作,那么采取任何类型的锁的开销都会更多比检查参数是否超出范围的检查要好,仅在写锁定内进行检查可能会更好。
詹姆斯

此类似乎也不是很有用,因为基于偏移量的函数(大多数)不能真正安全地使用,除非总有一些外部锁定方案,因为在您决定放置位置或放置位置之间,集合可能会发生变化。从中以及何时实际获得东西。
詹姆斯

1
我想继续记录说,我认识到IList语义在并发场景中的用处最多是有限的。我可能在实现之前就编写了这段代码。我的经验与接受答案的作者相同:我尝试了一下有关同步和IList <T>的知识,并从中学到了一些东西。
罗尼·欧弗比
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.