【问题标题】:Thread-safe List<T> property线程安全的 List<T> 属性
【发布时间】:2011-08-17 23:23:45
【问题描述】:

我想要一个 List&lt;T&gt; 的实现作为一个可以毫无疑问地线程安全使用的属性。

类似这样的:

private List<T> _list;

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

我似乎仍然需要返回一个集合的副本(克隆),所以如果我们在某个地方迭代集合并且同时设置了集合,那么不会引发异常。

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

【问题讨论】:

  • 使用锁,应该这样做。
  • 可以使用IList&lt;T&gt;(与List&lt;T&gt;相比)的线程安全实现吗?
  • 你检查过SynchronizedCollection<T> 吗?
  • 使用 BlockingCollection 或 ConcurrentDictionary
  • 属性后面的对象需要做哪些操作?您是否可能不需要List&lt;T&gt; 实现的所有功能?如果是,那么您能否提供一个您需要的界面,而不是询问List&lt;T&gt; 已经拥有的所有内容?

标签: c# collections properties thread-safety


【解决方案1】:

使用lock 语句来执行此操作。 (Read here for more information.)

private List<T> _list;

private List<T> MyT
{
    get { return _list; }
    set
    {
        //Lock so only one thread can change the value at any given time.
        lock (_list)
        {
            _list = value;
        }
    }
}

仅供参考,这可能不是您所要求的——您可能想在代码中锁定更远的位置,但我不能假设。查看 lock 关键字并根据您的具体情况调整其用法。

如果需要,您可以在 getset 块中使用 _list 变量,这将使其不会同时发生读/写操作。

【讨论】:

  • 这不能解决他的问题;它只会阻止线程设置引用,而不是添加到列表中。
  • 如果一个线程正在设置值,而另一个线程正在迭代集合怎么办(这可能与您的代码有关)。
  • 就像我说的,锁可能必须在代码中进一步移出。这只是一个如何使用 lock 语句的示例。
  • @Joel Mueller:当然,如果你制造了一些这样的愚蠢例子。我只是想说明提问者应该查看lock 声明。使用一个类似的例子,我可以争辩说我们不应该使用 for 循环,因为你可以毫不费力地使应用程序死锁:for (int x = 0; x &gt;=0; x += 0) { /* Infinite loop, oops! */ }
  • 我从来没有声称你的代码意味着即时死锁。由于以下原因,这是对这个特定问题的错误答案:1)它不能防止列表的内容在列表枚举期间被修改,或者同时被两个线程修改。 2) 锁定 setter 而不是 getter 意味着该属性不是真正的线程安全的。 3) 锁定可从类外部访问的任何 引用被广泛认为是一种不好的做法,因为它大大增加了意外死锁的机会。这就是为什么 lock (this)lock (typeof(this)) 是大禁忌。
【解决方案2】:

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

在这种情况下,您可以使用ConcurrentBag&lt;T&gt; 而不是List&lt;T&gt;

【讨论】:

  • Like List 与 Dictionary 不同的是,ConcurrentBag 接受重复项。
  • ConcurrentBag 是无序集合,因此与List&lt;T&gt; 不同,它不保证有序。您也不能按索引访问项目。
  • @RadekStromský 是对的,如果你想要一个有序的并发列表,你可以试试ConcurrentQueue (FIFO)ConcurrentStack (LIFO)
  • ConcurrentBag 没有实现 IList 并且实际上不是 List 的线程安全版本
【解决方案3】:

基本上如果要安全枚举,就需要使用lock。

请参考 MSDN。 http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx

这是您可能感兴趣的 MSDN 的一部分:

这种类型的公共静态(在 Visual Basic 中为共享)成员是线程安全的。不保证任何实例成员都是线程安全的。

只要集合不被修改,一个 List 可以同时支持多个读取器。通过集合枚举本质上不是线程安全的过程。在枚举与一个或多个写访问竞争的极少数情况下,确保线程安全的唯一方法是在整个枚举期间锁定集合。要允许集合被多个线程访问以进行读写,您必须实现自己的同步。

【讨论】:

  • 完全不正确。您可以使用并发集。
【解决方案4】:

我相信_list.ToList() 会给你一个副本。如有需要也可以查询如:

_list.Select("query here").ToList(); 

无论如何,msdn 说这确实是一个副本,而不仅仅是一个参考。哦,是的,您需要像其他人指出的那样锁定 set 方法。

【讨论】:

    【解决方案5】:

    我认为制作一个示例 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;
        }
    }
    

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

    【讨论】:

    • 这不是浅克隆吗?如果T 是一个引用类型,这不只是返回一个包含对所有原始对象的引用的新列表吗?如果是这种情况,这种方法仍然可能导致线程问题,因为多个线程可以通过列表的不同“副本”访问列表对象。
    • 正确,是浅拷贝。关键是要简单地拥有一个可以安全迭代的克隆集(因此newList 没有添加或删除任何会使枚举器无效的项目)。
    • _lock 应该是静态的吗?
    • 另一个想法。这个实现对于多个作者来说是线程安全的吗?如果不是,也许它应该被称为 ReadSafeList。
    • @MikeWard - 我认为不应该,当 any 实例被克隆时,all 实例将被锁定!
    【解决方案6】:

    你也可以使用更原始的

    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 的简单性 + 透明性 + 它在阻止崩溃方面发挥了重要作用

    【讨论】:

      【解决方案7】:

      即使它获得了最多的选票,通常也不能将System.Collections.Concurrent.ConcurrentBag&lt;T&gt; 作为System.Collections.Generic.List&lt;T&gt; 的线程安全替代品,因为它(Radek Stromský 已经指出)没有被订购。

      但是有一个名为 System.Collections.Generic.SynchronizedCollection&lt;T&gt; 的类自 .NET 3.0 起就已成为框架的一部分,但它隐藏得很好偶然发现它(至少我从未这样做过)。

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

      【讨论】:

      • 对此选项的其他有用讨论:stackoverflow.com/a/4655236/12484
      • @denfromufa 看起来他们在 .net core 2.0 中添加了这个 docs.microsoft.com/en-gb/dotnet/api/…
      • ConcurrentBag 不是列表的替代品。它的行为不像列表,你不能删除像列表这样的元素。在列表中您可以指定要删除的项目,您不能对并发行李执行此操作
      【解决方案8】:

      即使接受的答案是 ConcurrentBag,我不认为它在所有情况下都可以真正替代列表,因为 Radek 对答案的评论说:“ConcurrentBag 是无序集合,因此与 List 不同,它不保证排序。您也无法访问按索引列出的项目”。

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

      【讨论】:

      • 请提供-1的原因
      • 我没有投反对票,国际海事组织也没有理由这样做。您是对的,但是某些答案中已经提到了这个概念。对我来说,关键是 .NET 4.0 中有一个我不知道的新线程安全集合。不确定是否使用 Bag 或 Collection 来解决这种情况。 +1
      • 这个答案有几个问题:1)ConcurrentDictionary 是字典,不是列表。 2) 正如您自己的答案所述,不能保证保留顺序,这与您发布答案的陈述理由相矛盾。 3)它链接到一个 video 而不将相关引用带入这个答案(这可能与他们的许可不一致)。
      • 如果文档没有明确保证,您不能依赖 current implementation 之类的东西。实施可能随时更改,恕不另行通知。
      • 我认为这个建议很有潜力。如果 Dictionary.Count 用作键(如果没有删除),则任何线程都可以添加类似 while (!myDict.TryAdd(myDict.Count, myValue)) { } 的值(或使用计数器的原子增量以防有删除)。这将保证在检索它们时可以将这些值带入原始顺序。
      【解决方案9】:

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

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

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

      【讨论】:

      • 你说的是什么语言?
      • Java?我想念它的少数功能之一。但它通常写成:Collections.synchronizedList(new ArrayList());
      • 这是有效的 C# 假设​​你有一个 using System.Collections 或者你可以使用 var System.Collections.ArrayList.Synchronized(new System.Collections.ArrayList());
      【解决方案10】:

      如果您查看 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 创建更好的实现,但没有实现。

      【讨论】:

      • +1 在每次访问(读/写)中锁定整个集合 (_root) 使得这个解决方案很慢。也许这个类留在内部会更好。
      • 这个实现不是线程安全的。它仍然抛出“System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'”
      • 这与线程安全无关,而是与您正在迭代和更改集合这一事实有关。当枚举器看到列表已更改时,会引发异常。要解决这个问题,您需要实现自己的 IEnumerator 或更改代码,以便它不会同时迭代和更改同一个集合。
      • 它不是线程安全的,因为集合可以在“同步”方法期间更改。这绝对线程安全的一部分。考虑一个线程在另一个调用this[index] 之后但在锁被激活之前调用Clear()index 不再安全使用,最终执行时会抛出异常。
      【解决方案11】:

      似乎很多发现这个问题的人都想要一个线程安全的索引动态大小的集合。我所知道的最接近和最简单的事情是。

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

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

      【讨论】:

      • 如果不是钥匙的错,为什么还要认定钥匙有罪?
      • @Suncat2000 公顷!
      【解决方案12】:

      我建议任何在多线程场景中处理List&lt;T&gt; 的人查看Immutable Collections,尤其是ImmutableArray

      我发现它非常有用:

      1. 列表中的项目相对较少
      2. 没有那么多读/写操作
      3. 大量并发访问(即许多线程以读取模式访问列表)

      当您需要实现某种类似事务的行为(即在失败的情况下恢复插入/更新/删除操作)时也很有用

      【讨论】:

        【解决方案13】:

        在.NET Core(任何版本)中,您可以使用ImmutableList,它具有List&lt;T&gt;的所有功能。

        【讨论】:

          【解决方案14】:

          这里是没有锁的线程安全列表类

           public class ConcurrentList   
              {
                  private long _i = 1;
                  private ConcurrentDictionary<long, T> dict = new ConcurrentDictionary<long, T>();  
                  public int Count()
                  {
                      return dict.Count;
                  }
                   public List<T> ToList()
                   {
                      return dict.Values.ToList();
                   }
          
                  public T this[int i]
                  {
                      get
                      {
                          long ii = dict.Keys.ToArray()[i];
                          return dict[ii];
                      }
                  }
                  public void Remove(T item)
                  {
                      T ov;
                      var dicItem = dict.Where(c => c.Value.Equals(item)).FirstOrDefault();
                      if (dicItem.Key > 0)
                      {
                          dict.TryRemove(dicItem.Key, out ov);
                      }
                      this.CheckReset();
                  }
                  public void RemoveAt(int i)
                  {
                      long v = dict.Keys.ToArray()[i];
                      T ov;
                      dict.TryRemove(v, out ov);
                      this.CheckReset();
                  }
                  public void Add(T item)
                  {
                      dict.TryAdd(_i, item);
                      _i++;
                  }
                  public IEnumerable<T> Where(Func<T, bool> p)
                  {
                      return dict.Values.Where(p);
                  }
                  public T FirstOrDefault(Func<T, bool> p)
                  {
                      return dict.Values.Where(p).FirstOrDefault();
                  }
                  public bool Any(Func<T, bool> p)
                  {
                      return dict.Values.Where(p).Count() > 0 ? true : false;
                  }
                  public void Clear()
                  {
                      dict.Clear();
                  }
                  private void CheckReset()
                  {
                      if (dict.Count == 0)
                      {
                          this.Reset();
                      }
                  }
                  private void Reset()
                  {
                      _i = 1;
                  }
              }
          

          【讨论】:

          • _i++ 不是线程安全的。当你增加它时你必须使用原子添加,并且可能也将它标记为 volatile。 CheckReset() 不是线程安全的。在条件检查和调用 Reset() 之间可能发生任何事情。不要编写自己的多线程实用程序。
          【解决方案15】:

          查看原始示例可能会猜到其意图是能够简单地用新列表替换列表。属性的 setter 告诉我们。

          Micrisoft 的线程安全集合用于从集合中安全地添加和删除项目。但是,如果在应用程序逻辑中您打算用新的集合替换集合,人们可能会再次猜测,不需要 List 的添加和删除功能。

          如果是这种情况,简单的答案是使用 IReadOnlyList 接口:

           private IReadOnlyList<T> _readOnlyList = new List<T>();
          
              private IReadOnlyList<T> MyT
              {
                 get { return _readOnlyList; }
                 set { _readOnlyList = value; }
              }
          

          在这种情况下不需要使用任何锁定,因为无法修改集合。如果在设置器中“_readOnlyList = value;”将被更复杂的东西取代,然后可能需要锁。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-11-26
            • 1970-01-01
            • 2014-08-04
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多