C#多线程列表操作

时间:2010-02-13 13:00:22

标签: c# multithreading atomic

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

class A
{
    List<SomeClass> list;

    private void clearList()
    {
        list = new List<SomeClass>();
    }

    private void addElement()
    {
        list.Add(new SomeClass(...));
    }
}

当两个函数并行执行时,我是否可能遇到多线程问题(或任何类型的意外行为)?

用例是一个错误列表,可以随时清除(通过简单地指定一个新的空列表)。

编辑:我的假设是

  • 只有一个线程添加了元素
  • 忘记的元素是可以的(即清除和添加新元素之间的竞争条件),只要清除操作成功没有问题
  • .NET 2.0

6 个答案:

答案 0 :(得分:10)

这里有两种可能的问题:

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

鉴于您正在访问共享资源,我会在访问时亲自持有锁。您仍然需要考虑在添加项目之前/之后立即清除列表的可能性。

编辑:我的评论如果你只是从一个帖子中添加就没关系,这已经有点可疑了,原因有两个:

  • 可能(我认为!)你最终可能会尝试添加到尚未完全构建的List<T>。我不确定,.NET 2.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);
        }
    }

}

答案 1 :(得分:2)

是的 - 有可能,。事实上,如果真的同时被调用,那很有可能。

此外,如果同时发生对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(...));
        }
    }
}

答案 2 :(得分:2)

.NET中的集合(最多3.5个)不是线程安全的或非阻塞的(并行执行)。您应该通过从IList派生并使用ReaderWriterLockSlim来执行每个操作来实现您的。例如,您的Add方法应如下所示:

    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();
    }

注意:实现线程安全或并行集合(实际上是每个其他类)时,请不要从类中导出,但要接口!因为总会出现与该类的内部结构相关的问题,或者某些非虚拟的方法,您必须隐藏它们等等。如果你必须这样做,请非常小心!

答案 3 :(得分:1)

如果要清除它,只需创建一个新列表就不是一件好事。

我假设您还在构造函数中分配了列表,因此您不会遇到空指针异常。

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

查看.Net 4新集合来处理多线程任务:)

<强>此外: 如果你使用.Net 4,请查看名称空间System.Collections.Concurrent。在那里你会发现:System.Collections.Concurrent.ConcurrentBag<T>和许多其他很好的集合:)

你还应该注意,如果你不注意,锁可以显着降低性能。

答案 4 :(得分:1)

如果在多个线程中使用此类的一个实例,是的。你会遇到问题。 .Net框架(3.5及更低版本)中的所有集合都不是线程安全的。特别是当你开始更改集合而另一个线程正在迭代它时。

在多线程环境中使用锁定并提供“复制”集合,或者如果可以使用.Net 4.0,则使用新的并发集合。

答案 5 :(得分:0)

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

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

据我了解,它仍然会造成麻烦。这完全取决于如何在硬件级别上实现引用分配。更准确地说,这个操作是否是原子操作。

我认为,尽管存在很小的问题,但仍有机会,特别是在多处理器环境中,该进程将被损坏的引用,因为它在访问它时只是部分更新。