线程安全的 List<T> 属性

183

我希望有一个可以在多线程下安全使用的 List<T> 属性的实现。

类似于这样:

private List<T> _list;

private List<T> MyT
{
    get { // return a copy of _list; }
    set { _list = value; }
}

看起来我仍然需要返回集合的副本,这样如果我们在遍历集合的同时对集合进行设置,则不会引发异常。

如何实现一个线程安全的集合属性?


8
使用锁,这样就可以了。 - atoMerz
可以使用线程安全的 IList<T> 实现(而不是 List<T>)吗? - Greg
3
你有检查过SynchronizedCollection<T>吗? - Roi Shabtai
1
使用BlockingCollection或ConcurrentDictionary。 - kumar chandraketu
这可能是所有优秀程序员在他们的生涯中迟早会问的问题。在所有List操作周围使用lock(...)似乎是正确的答案。 - thomasgalliker
显示剩余2条评论
15个回答

237
如果你的目标是 .Net 4,那么在 System.Collections.Concurrent 命名空间中有一些选项可供选择。
在这种情况下,你可以使用 ConcurrentBag<T> 而不是 List<T>

9
与Dictionary不同,ConcurrentBag允许重复项,类似于List<T>。 - The Light
174
ConcurrentBag 是一个无序的集合,因此与 List<T> 不同,它不保证顺序。而且你不能通过索引访问元素。 - Radek Stromský
17
@RadekStromský是正确的,如果您想要一个有序的并发列表,可以尝试使用ConcurrentQueue(FIFO)ConcurrentStack(LIFO) - Caio Cunha
13
可能是SynchronizedCollection<T>吗? - Roi Shabtai
19
ConcurrentBag没有实现IList接口,也不是List的线程安全版本。 - Vasyl Zvarydchuk
显示剩余5条评论

132
虽然在得票最高的情况下,通常无法将 System.Collections.Concurrent.ConcurrentBag<T> 视为 System.Collections.Generic.List<T> 的线程安全替代品,因为它(Radek Stromský 已经指出)不是有序的。
但是已经存在一个名为 System.Collections.Generic.SynchronizedCollection<T> 的类,自 .NET 3.0 起已成为框架的一部分,但它被隐藏在一个意想不到的位置中,以至于鲜为人知,可能你从未遇到过它(至少我没有)。 SynchronizedCollection<T> 编译成程序集 System.ServiceModel.dll 中(这是客户端配置文件的一部分,但不是可移植类库)。

1
此选项的其他有用讨论:https://dev59.com/rW445IYBdhLWcg3w499e#4655236 - Jon Schneider
7
@denfromufa 看起来他们在 .NET Core 2.0 中添加了这个 https://learn.microsoft.com/en-gb/dotnet/api/system.collections.generic.synchronizedcollection-1?view=netframework-4.7.1#Applies_to - Cirelli94
4
ConcurrentBag不能替代列表。它的行为不像列表,你不能像操作列表一样移除元素。在列表中,你可以指定要移除的项,但在ConcurrentBag中不能这样做。 - amarnath chatterjee
不幸的是,这并不完全是线程安全的。枚举不是线程安全的,这也是为什么人们会选择它而不是其他类型的主要原因之一。 - Ivan Ičin
我刚刚使用ConcurrentBag替换List破坏了我的网站。我以为它可能会失败,但我不确定。如果有一个类似的并发列表,那就会容易得多了。我想下次尝试一下Stack :) - Simon_Weaver

21

我认为创建一个示例ThreadSafeList类应该很容易:

public class ThreadSafeList<T> : IList<T>
{
    protected List<T> _internalList = new List<T>();

    // Other Elements of IList implementation

    public IEnumerator<T> GetEnumerator()
    {
        return Clone().GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return Clone().GetEnumerator();
    }

    protected static object _lock = new object();

    public List<T> Clone()
    {
        List<T> newList = new List<T>();

        lock (_lock)
        {
            _internalList.ForEach(x => newList.Add(x));
        }

        return newList;
    }
}

在请求枚举器之前,你可以简单地克隆列表,这样任何枚举都是基于一个不能在运行时修改的副本进行的。


4
没错,这是一个浅拷贝。重点是简单地创建一个克隆集合,以便可以安全地对其进行迭代(因此newList不会添加或删除任何项目,这将使枚举器无效)。 - Tejs
13
这个_lock应该是静态的吗? - Mike Ward
5
另一个想法。这个实现对于多个写入者来说是线程安全的吗?如果不是,也许它应该被称为“ReadSafeList”。 - Mike Ward
11
@MikeWard - 我认为不应该,任何一个实例被克隆时,所有实例都会被锁定! - Josh M.
1
你的 Clone() 方法可以简单地写成 lock(_lock) { return new List<T>(_internalList); } - Mr Anderson
显示剩余7条评论

19

尽管被接受的答案是ConcurrentBag,但我认为它并非所有情况下都能真正替代列表,正如Radek对答案的评论所说:“ConcurrentBag是无序集合,因此与List不同,它不能保证排序。此外,您无法通过索引访问项目。”

因此,如果您使用.NET 4.0或更高版本,一种解决方法是使用具有整数TKey作为数组索引和TValue作为数组值的ConcurrentDictionary。这是Pluralsight的C# Concurrent Collections course中替换列表的推荐方法。ConcurrentDictionary解决了上述两个问题:索引访问和排序(我们不能依赖排序,因为它在内部是哈希表,但当前的.NET实现保存了添加元素的顺序)。


我没有给你的回答点踩,而且在我看来也没有理由这么做。你说得没错,但是这个概念已经在一些回答中提到了。对我来说,重点是在.NET 4.0中有一个新的线程安全集合,而我之前并不知道。不确定在这种情况下使用Bag还是Collection。+1 - Xaqron
4
这个回答有几个问题:1)ConcurrentDictionary 是一个字典,不是一个列表。2)正如你自己的回答所述,它不能保证保留顺序,这与你发布回答的原因相矛盾。3)它链接到一个视频,而没有将相关的引用带入此答案中(这可能也与其许可证不符)。 - jpmc26
1
如果文档没有明确保证,你不能依赖于像“当前实现”这样的东西。实现可能随时更改而没有通知。 - Victor Yarema
1
@jpmc26,是的,它当然不是List的完全替代品,但是ConcurrentBag作为一个被接受的答案也是如此——它并不是List的严格替代品,而是一种解决方法。回答您的疑虑:1)ConcurrentDictionary是一个字典而不是列表,您是正确的,但是列表背后有一个数组,可以像使用int作为键的字典一样以O(1)的速度进行索引。2)是的,顺序在文档中不能保证(尽管它被保留了下来),但是被接受的ConcurrentBag在多线程场景下也无法保证顺序。 - tytyryty
1
在我看来,那个建议很有潜力。如果使用Dictionary.Count作为键(在没有删除的情况下),任何线程都可以像这样添加值 while (!myDict.TryAdd(myDict.Count, myValue)) { }(或者在可能存在删除的情况下使用计数器的原子增量)。这将确保在检索值时可以将它们带回原始顺序。 - Christoph

19

C#的ArrayList类有一个Synchronized方法。

var threadSafeArrayList = ArrayList.Synchronized(new ArrayList());

这将返回一个线程安全的包装器,可以包装任何IList实例。为确保线程安全,所有操作都需要通过包装器执行。


1
你在说哪种编程语言? - John Demetriou
Java?我最怀念的几个特性之一。但通常它被写成:Collections.synchronizedList(new ArrayList()); - Nick
2
假设您使用了 using System.Collections,那么这是有效的 C# 代码。 或者您可以使用 var System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList());。 - user2163234
它不是通用的 :( - Mr Patience

11
在任何版本的.NET Core中,您可以使用ImmutableList,它具有与List<T>相同的所有功能。请注意,ImmutableList是不可变集合,意味着一旦创建,便不能更改其内容。

是的,但你仍然需要将其包装在锁中 - 它是不可变的,因此当您修改它时,需要重新分配新列表,这不是线程安全的。 - Mr Patience
@MrPatience 不是的!List 的并发问题是一个线程在迭代它时,另一个线程对其进行了修改。在这种情况下,如果一个线程正在迭代列表,而另一个线程对其进行修改,则被迭代的列表保持不变,并创建一个新的修改后的列表,因此没有并发问题。请记住,对象是通过引用传递的,因此迭代线程仍然具有对原始未更改的列表的引用。如果您不相信,请查看文档:https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablelist-1#thread-safety - JotaBe

11

回复自我的所有答案:

人们发现了这些解决方案:

  • 使用SynchronizedCollection - 源代码与下面的SynchronizedList几乎相同,具有相同的功能和问题
  • 使用ArrayList.Synchronized - 这将返回SyncIList,它与下面的SynchronizedList相同,只是不是泛型
  • 使用线程安全的列表形式,在每次访问时克隆它 - 我认为使用ThreadLocal或AsyncLocal可以显着改进所提供的实现,但它仍然无法通过任何性能测试
  • 使用Collections.Concurrent命名空间中的各种类的组合 - 这些包含了一些适用于ICollection的好选择,但对于索引访问的IList则不是
  • 使用ConcurrentDictionary<int, T>,以索引作为键来模拟IList - 这是我见过的最好的想法之一,但它并不真正符合IList的精神,这意味着O(1)的索引读取/追加复杂度和插入/删除的一些复杂度,以及O(n)的空间复杂度。此外,IndexOf和排序操作怎么办?

SynchronizedList类最常见的投诉是:

  • 锁机制的缓慢 - 性能计算因使用列表的情况而异,因此这是一个模糊需求的有效选项
  • 使用lock(object)而不是SemaphoreSlim - 好吧,这是我的投诉 :) 但将代码修复为使用它是微不足道的

可以实现更复杂的锁系统,例如针对单个行、一组行等。尽管如此,这将开始看起来像是实现自己的数据库。编写高性能集合是一门艺术,与您想要使用它的具体场景紧密相连。

我仍然相信对于一般使用场景,简单的解决方案是最通用的。

C5 collection库是伟大收藏设计的灵感之一,处理并发问题的方式如下:

  • 无并发收集(按设计)因为他们认为可以实现简单的锁机制,但当多个收集同时被访问时,情况变得非常复杂
  • "基于树的收集在修改时可以安全地枚举" - 在这里他们推荐"模式66":"可以拍摄树的快照,然后枚举该快照的项,同时修改原始树"

原始答案:

如果您查看T列表的源代码(https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,c66df6f36c131877),您将会发现有一个类叫做SynchronizedList of T(当然是内部的 - 为什么,微软,为什么?!?!)。我在这里复制粘贴了代码:
   [Serializable()]
    internal class SynchronizedList : IList<T> {
        private List<T> _list;
        private Object _root;

        internal SynchronizedList(List<T> list) {
            _list = list;
            _root = ((System.Collections.ICollection)list).SyncRoot;
        }

        public int Count {
            get {
                lock (_root) { 
                    return _list.Count; 
                }
            }
        }

        public bool IsReadOnly {
            get {
                return ((ICollection<T>)_list).IsReadOnly;
            }
        }

        public void Add(T item) {
            lock (_root) { 
                _list.Add(item); 
            }
        }

        public void Clear() {
            lock (_root) { 
                _list.Clear(); 
            }
        }

        public bool Contains(T item) {
            lock (_root) { 
                return _list.Contains(item);
            }
        }

        public void CopyTo(T[] array, int arrayIndex) {
            lock (_root) { 
                _list.CopyTo(array, arrayIndex);
            }
        }

        public bool Remove(T item) {
            lock (_root) { 
                return _list.Remove(item);
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
            lock (_root) { 
                return _list.GetEnumerator();
            }
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator() {
            lock (_root) { 
                return ((IEnumerable<T>)_list).GetEnumerator();
            }
        }

        public T this[int index] {
            get {
                lock(_root) {
                    return _list[index];
                }
            }
            set {
                lock(_root) {
                    _list[index] = value;
                }
            }
        }

        public int IndexOf(T item) {
            lock (_root) {
                return _list.IndexOf(item);
            }
        }

        public void Insert(int index, T item) {
            lock (_root) {
                _list.Insert(index, item);
            }
        }

        public void RemoveAt(int index) {
            lock (_root) {
                _list.RemoveAt(index);
            }
        }
    }

个人认为他们知道可以使用SemaphoreSlim创建更好的实现,但是没有去做。


3
将整个集合(_root)在每次访问(读/写)时锁定会使其变得缓慢。也许让这个类保持内部状态会更好。 - Xaqron
4
此实现不是线程安全的。它仍会抛出“System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'”。 - Raman Zhylich
5
这与线程安全无关,而是因为您正在迭代和更改集合。当枚举器发现列表已更改时,会抛出异常。为了解决这个问题,您需要实现自己的IEnumerator或更改代码,使其不同时迭代和更改同一集合。 - Siderite Zackwehdex
3
它不是线程安全的,因为集合在“同步”方法期间可能会被更改。这绝对是线程安全的一部分。考虑一个线程在另一个线程调用this[index]之后但在锁定被激活之前调用了Clear()index不再安全使用,并且最终执行时将抛出异常。 - Suncat2000
2
你可以通过在单线程中引用列表,然后调用另一个清空列表的函数,再尝试直接按索引访问元素来轻松地引起相同的问题。基于索引的访问应该防范这些异常,但我确实理解你关注使用友好性的观点。 - Kemuel Sanchez
显示剩余4条评论

2
我建议在多线程场景中处理 List<T> 的任何人都应该看看不可变集合,特别是ImmutableArray
当您拥有:
  1. 相对较少的列表项
  2. 不那么频繁的读/写操作
  3. 大量并发访问(即许多线程以读模式访问列表)
时,我发现它非常有用。
此外,当您需要实现某种类似于事务的行为(例如,在失败的情况下撤消插入/更新/删除操作)时也很有用。

2

看起来很多人想要一个线程安全的、索引动态大小的集合。我所知道的最接近且最容易使用的东西是。

System.Collections.Concurrent.ConcurrentDictionary<int, YourDataType>

这将需要您确保键适当地增加,如果想要正常的索引行为。如果您小心使用 .count(),它可以作为您添加的任何新键值对的键。

8
为什么钥匙没有问题,却要将其定罪?这与钥匙无关。 - Suncat2000
@Suncat2000 哈! - Richard II

1

您也可以使用更原始的方法

Monitor.Enter(lock);
Monitor.Exit(lock);

使用哪种锁(请参见此帖子C# Locking an object that is reassigned in lock block)。

如果您期望代码中出现异常,则这不是安全的,但它允许您执行以下操作:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Linq;

public class Something
{
    private readonly object _lock;
    private readonly List<string> _contents;

    public Something()
    {
        _lock = new object();

        _contents = new List<string>();
    }

    public Modifier StartModifying()
    {
        return new Modifier(this);
    }

    public class Modifier : IDisposable
    {
        private readonly Something _thing;

        public Modifier(Something thing)
        {
            _thing = thing;

            Monitor.Enter(Lock);
        }

        public void OneOfLotsOfDifferentOperations(string input)
        {
            DoSomethingWith(input);
        }

        private void DoSomethingWith(string input)
        {
            Contents.Add(input);
        }

        private List<string> Contents
        {
            get { return _thing._contents; }
        }

        private object Lock
        {
            get { return _thing._lock; }
        }

        public void Dispose()
        {
            Monitor.Exit(Lock);
        }
    }
}

public class Caller
{
    public void Use(Something thing)
    {
        using (var modifier = thing.StartModifying())
        {
            modifier.OneOfLotsOfDifferentOperations("A");
            modifier.OneOfLotsOfDifferentOperations("B");

            modifier.OneOfLotsOfDifferentOperations("A");
            modifier.OneOfLotsOfDifferentOperations("A");
            modifier.OneOfLotsOfDifferentOperations("A");
        }
    }
}

其中一个好处是,在一系列操作期间,您将获得锁定(而不是在每个操作中锁定)。这意味着输出应该以正确的块形式出现(我使用它从外部进程将一些输出显示在屏幕上)

我真的很喜欢ThreadSafeList的简单性和透明度,它在防止崩溃方面扮演了重要角色。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接