线程安全List <T>属性

Xaq*_*ron 108 c# collections properties thread-safety

我想要List<T>一个属性的实现,可以毫无疑问地使用线程安全.

像这样的东西:

private List<T> _list;

private List<T> MyT
{
    get { // return a copy of _list; }
    set { _list = value; }
}
Run Code Online (Sandbox Code Playgroud)

看来我仍然需要返回一个集合(克隆)的集合,所以如果我们在某个地方迭代集合并同时设置集合,那么就不会引发异常.

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

Bal*_*a R 173

如果您的目标是.Net 4,System.Collections.Concurrent命名空间中有几个选项

你可以ConcurrentBag<T>在这种情况下使用而不是List<T>

  • `ConcurrentBag`是无序集合,因此与`List <T>`不同,它不保证订购.您也无法通过索引访问项目. (91认同)
  • @RadekStromský是对的,如果您想要一个有序的并发列表,您可以尝试[ConcurrentQueue(FIFO)](http://msdn.microsoft.com/en-us/library/dd267265(v = vs.110) .aspx)或[ConcurrentStack(LIFO)](http://msdn.microsoft.com/en-us/library/dd267331(v = vs.110).aspx). (11认同)
  • ConcurrentBag没有实现IList,实际上并不是List的线程安全版本 (8认同)
  • 也许[SynchronizedCollection <T>](https://msdn.microsoft.com/en-us/library/ms668265(v = vs.110).aspx)? (7认同)
  • 与List <T>类似,与Dictionary不同,ConcurrentBag接受重复. (4认同)
  • 并且`ConcurrentBag`为每个线程创建其集合的副本,因此这可能会产生误导.此外,诸如移除和添加之类的操作表现不同. (3认同)
  • @VasylZvarydchuk-从技术上讲,这不是列表。但这正是大多数人在使用List时需要的,但需要线程安全的东西。 (2认同)
  • @DonCheadle除了你也不能从中删除项目。我还没有找到一个有用的案例。 (2认同)
  • ConcurrentBag没有删除功能。如果您需要从列表中删除,则不建议这样做 (2认同)

Chr*_*oph 81

即使获得最多的选票,人们通常也不能将其System.Collections.Concurrent.ConcurrentBag<T>作为线程安全的替代品System.Collections.Generic.List<T>(RadekStromský已经指出)没有订购.

但是有一个类System.Collections.Generic.SynchronizedCollection<T>已经被称为.NET 3.0已经是框架的一部分了,但是它隐藏在一个人们不期望的地方,它鲜为人知,也许你从来没有偶然发现它(至少我从没干过).

SynchronizedCollection<T>被编译成程序集System.ServiceModel.dll(它是客户端配置文件的一部分,但不是可移植类库的一部分).

希望有所帮助.

  • @denfromufa 看起来他们在 .net core 2.0 https://docs.microsoft.com/en-gb/dotnet/api/system.collections.generic.synchronizedcollection-1?view=netframework-4.7.1#Applies_to 中添加了这个 (5认同)
  • ConcurrentBag 不是列表的替代品。它的行为不像列表,您不能像列表一样删除元素。在列表中,您可以指定要删除的项目,但不能对并发行李执行此操作 (5认同)
  • 我哭了,这不是核心库:{一个简单的同步集合通常都是需要的. (3认同)
  • 并且在.net核心中不可用 (3认同)
  • 此选项的其他有用讨论:http://stackoverflow.com/a/4655236/12484 (2认同)
  • 它是隐藏的,因为不推荐使用,而推荐使用System.Collections.Concurrent中的类。 (2认同)

Tej*_*ejs 15

我认为制作一个示例ThreadSafeList类很容易:

public class ThreadSafeList<T> : IList<T>
{
    protected List<T> _interalList = 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)
        {
            _interalList.ForEach(x => newList.Add(x));
        }

        return newList;
    }
}
Run Code Online (Sandbox Code Playgroud)

您只需在请求枚举器之前克隆列表,因此任何枚举都在处理运行时无法修改的副本.

  • _lock应该是静态的吗? (7认同)
  • @MikeWard - 我不认为它应该是****所有**实例将在**任何**实例被克隆时锁定! (5认同)
  • 另一种想法.这个实现是否适用于多个编写器?如果没有,也许它应该被称为ReadSafeList. (4认同)
  • 正确,这是一个浅的副本.关键是要简单地拥有一个可以安全迭代的克隆集(因此`newList`没有添加或删除任何会使枚举器无效的项目). (2认同)
  • 当微软现在已经做出了许多不同类型的并发替代方案时,为什么要“重新发明方轮”呢? (2认同)

Sid*_*dex 8

如果您查看 T 列表(https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,c66df6f36c131877)的源代码,您会注意到那里有一个类(当然是内部 - 为什么,微软,为什么?!?!)称为 T 的 SynchronizedList。我在这里复制粘贴代码:

   [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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

  • 这与线程安全无关,而是与您正在迭代和更改集合的事实有关。枚举器在看到列表已更改时抛出异常。要解决此问题,您需要实现自己的 IEnumerator 或更改代码,使其不会同时迭代和更改相同的集合。 (5认同)
  • 此实现不是线程安全的。它仍然抛出“System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'” (3认同)
  • 它不是线程安全的,因为集合可以在“同步”方法期间更改。这绝对是线程安全的一部分。考虑一个线程在另一个线程调用“this[index]”之后但在锁被激活之前调用“Clear()”。`index` 不再可以安全使用,并且在最终执行时会抛出异常。 (3认同)
  • +1 在每次访问(读/写)中锁定整个集合(`_root`)使其成为一个缓慢的解决方案。也许这个类最好保留在内部。 (2认同)
  • 好吧,通过引用列表,然后调用另一个清除列表的函数,然后尝试通过索引直接访问元素,您可以很容易地在单线程中引起相同的问题。基于索引的访问应该防范这些异常,恕我直言,但我确实看到了你的使用友好性的观点。 (2认同)

Jot*_*aBe 7

在 .NET Core(任何版本)中,您可以使用ImmutableList,它具有List<T>.


tyt*_*yty 6

即使接受的答案是ConcurrentBag,我不认为它在所有情况下都是真正的替换列表,因为Radek对答案的评论说:"ConcurrentBag是无序集合,因此与List不同,它不保证订购.而且你不能通过索引访问项目".

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

  • 请提供-1的理由 (2认同)
  • 这个答案有几个问题:1)`ConcurrentDictionary`是字典,而不是列表。2)不能保证按照您自己的答案说明保存顺序,这与您发布答案的陈述理由相矛盾。3)它链接到*视频*,而没有在此答案中引入相关引号(反正可能与许可不符)。 (2认同)

小智 5

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

var threadSafeArrayList = ArrayList.Synchronized(new ArrayList());
Run Code Online (Sandbox Code Playgroud)

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

  • 假设您使用System.Collections或使用var System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList());这是有效的C#。 (2认同)