代码之家  ›  专栏  ›  技术社区  ›  AndiDog

C#多线程列表操作

  •  5
  • AndiDog  · 技术社区  · 14 年前

    如果我有这样的东西(伪代码):

    class A
    {
        List<SomeClass> list;
    
        private void clearList()
        {
            list = new List<SomeClass>();
        }
    
        private void addElement()
        {
            list.Add(new SomeClass(...));
        }
    }
    

    用例是一个错误列表,可以随时清除(只需分配一个新的空列表)。

    编辑: 我的假设是

    • 只要清除操作成功且没有问题,被遗忘的元素就可以了(即清除和添加新元素之间的竞争条件)
    • .NET2.0
    6 回复  |  直到 14 年前
        1
  •  10
  •   Jon Skeet    14 年前

    • 新添加的项目可能会立即被遗忘,因为您会清除并创建一个新列表。这是个问题吗?基本上,如果 AddElement ClearList 同时调用时,您有一个竞争条件:要么元素将在新列表中结束,要么在旧列表中结束(被遗忘)。
    • List<T> 对于多线程变异是不安全的,所以如果两个不同的线程调用 添加物

    编辑:我关于只从一个线程添加就可以了的评论已经有点可疑,原因有二:

    • 这是可能的(我想!)你最终可能会尝试添加到 列表<T> 还没有完全建成。我不确定,.NET2.0内存模型(与ECMA规范中的内存模型相反)可能足够强大,可以避免这种情况,但很难说。
    • 添加线程可能不会“看到”对 list 变量,并且仍然添加到旧列表中。事实上,没有 同步,它可以永远看到旧的值

    当您将“在GUI中迭代”添加到混合中时,它会变得非常棘手,因为您不能在迭代时更改列表。最简单的解决方案可能是提供一个返回 并且UI可以安全地迭代:

    class A
    {
        private List<SomeClass> list;
        private readonly object listLock = new object();
    
        private void ClearList()
        {
            lock (listLock)
            {
                list = new List<SomeClass>();
            }
        }
    
        private void AddElement()
        {
            lock (listLock)
            {
                list.Add(new SomeClass(...));
            }
        }
    
        private List<SomeClass> CopyList()
        {
            lock (listLock)
            {
                return new List<SomeClass>(list);
            }
        }
    
    }
    
        2
  •  2
  •   Rob Levine    14 年前

    此外,如果同时发生两个对addElement的单独调用,也可能导致问题。

    对于这种多线程处理,您确实需要在列表本身周围设置某种互斥锁,这样一次只能调用底层列表上的一个操作。

    围绕这一点的粗略锁定策略会有所帮助。比如:

    class A
    {
        static object myLock = new object()
        List<SomeClass> list;
    
        private void clearList()
        {
            lock(myLock)
            {
              list = new List<SomeClass>();
            }
    
        }
    
        private void addElement()
        {
            lock(myLock)
            {
              list.Add(new SomeClass(...));
            }
        }
    }
    
        3
  •  2
  •   Kaveh Shahbazian    14 年前

        public void Add(T item)
        {
            _readerWriterLockSlim.EnterWriteLock();
            try { _actualList.Add(item); }
            finally { _readerWriterLockSlim.ExitWriteLock(); }
        }
    

    您必须注意这里的一些并发技巧。例如,您必须有一个GetEnumerator,它以IList的形式返回一个新实例;不是实际的清单。否则你会遇到问题;应该是这样的:

        public IEnumerator<T> GetEnumerator()
        {
            List<T> localList;
    
            _lock.EnterReadLock();
            try { localList= new List<T>(_actualList); }
            finally { _lock.ExitReadLock(); }
    
            foreach (T item in localList) yield return item;
        }
    

    以及:

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)this).GetEnumerator();
        }
    

    注意:在实现线程安全或并行集合(事实上,每个其他类)时,不是从类派生,而是从接口派生!因为总有一些问题和那个类的内部结构或者一些非虚拟的方法有关,所以必须隐藏它们等等。如果必须这样做,请非常小心!

        4
  •  1
  •   Marvin Smit    14 年前

    在多线程环境中使用锁定并分发集合的副本,或者如果可以使用.Net 4.0,则使用新的并发集合。

        5
  •  1
  •   Ray Hayes    12 年前

    当你想清除一个新的列表时,仅仅创建一个新的列表并不是一件好事。

    如果清除并添加元素,可以将它们添加到旧列表中,我认为这样可以吗?但如果同时添加两个元素,则可能会遇到问题。

    如果使用.Net 4,请查看命名空间System.Collections.Concurrent。在那里您将发现: System.Collections.Concurrent.ConcurrentBag<T> 还有许多其他精美的收藏品:)

    您还应该注意,如果不小心,锁会显著降低性能。

        6
  •  0
  •   mfeingold    14 年前

    从对您的问题的编辑中可以清楚地看出,您并不真正关心这里常见的罪魁祸首——实际上没有同时调用同一对象的方法。

    本质上,您是在询问在从并行线程访问列表时是否可以将引用分配给列表。

    我认为,尽管如此,仍然有机会,特别是在多处理器环境中,进程将得到损坏的引用,因为它在访问它时只进行了部分更新。