List.Add()线程安全


87

我了解通常来说,列表不是线程安全的,但是如果线程从不对列表执行任何其他操作(例如遍历列表),则简单地将项目添加到列表中有什么错误吗?

例:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});


我曾经使用List <T>仅用于从并行运行的多个任务中添加新对象。有时,非常罕见,当在所有任务完成之后遍历列表时,它会以空记录结束,如果没有额外的线程参与,则实际上是不可能的。我猜想,当列表在内部重新分配其元素以进行扩展时,另一个线程通过尝试添加另一个对象以某种方式将其弄乱了。因此,这样做不是一个好主意!
osmiumbin

正是我目前看到的@osmiumbin关于一个对象,当简单地从多个线程中添加时,该对象莫名其妙地为null。感谢您的确认。
Blackey

Answers:


74

在幕后发生了很多事情,包括重新分配缓冲区和复制元素。该代码将导致危险。非常简单,添加到列表时没有原子操作,至少需要更新“长度”属性,并且需要将项放在正确的位置,并且(如果有单独的变量)索引也需要要被更新。多个线程可以相互踩踏。如果需要增长,那么还有更多的事情要做。如果某项内容正在写入列表,则其他任何内容都不应读取或写入。

在.NET 4.0中,我们有并发集合,它们很容易实现线程安全并且不需要锁。


完全有道理,为此,我一定会看看新的Concurrent系列。谢谢。
e36M3 2011年

11
请注意,没有内置ConcurrentList类型。有并发的包,字典,堆栈,队列等,但没有列表。
路加福音2011年

11

您当前的方法不是线程安全的-我建议完全避免这种情况-因为您基本上进行了数据转换PLINQ可能是一种更好的方法(我知道这是一个简化的示例,但最终您将每个事务投影到另一个“状态”中”对象)。

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();

我提出了一个过于简化的示例,以强调List.Add所感兴趣的地方。My Parallel.Foreach实际上将完成大量工作,并且将不是简单的数据转换。谢谢。
e36M3 2011年

4
如果不需要使用并发集合,那么可能会削弱并行性能-您可以做的另一件事是使用固定大小的数组,并使用Parallel.Foreach引入索引的重载-在这种情况下,每个线程都在操纵不同的数组项,因此您应该是安全的。
BrokenGlass 2011年

6

要求这不是不合理的事情。在某些情况下,如果它们是唯一调用的方法,则与其他方法结合使用会导致线程安全问题的方法是安全的。

但是,当考虑反射器中显示的代码时,显然不是这种情况:

public void Add(T item)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    this._items[this._size++] = item;
    this._version++;
}

即使EnsureCapacity它本身是线程安全的(并且肯定不是),考虑到同时调用增量运算符导致错误写入的可能性,上述代码显然也不是线程安全的。

在完成工作后,要么使用锁,要么使用ConcurrentList,要么使用无锁队列作为多个线程写入的位置,然后直接或通过填充列表从中读取(我假设从您的问题来看,在此同时进行多个同时写操作和单线程读取是您的模式,否则我无法看到Add调用唯一方法的条件有什么用。


6

如果要在List.add多个线程中使用并且不关心顺序,则可能无论如何都不需要索引能力List,而应该使用一些可用的并发集合。

如果您忽略此建议而仅这样做add,则可以使add线程安全,但顺序不可预测,如下所示:

private Object someListLock = new Object(); // only once

...

lock (someListLock)
{
    someList.Add(item);
}

如果您接受这种不可预测的排序,则很可能像前面提到的那样,您不需要像那样可以进行索引的集合someList[i]


5

这将引起问题,因为List是在数组之上构建的,并且不是线程安全的,因此您可能会导致索引超出范围异常或某些值将覆盖其他值,具体取决于线程的位置。基本上,不要这样做。

有多个潜在的问题……只是不要。如果需要线程安全集合,请使用锁或System.Collections.Concurrent集合之一。


5

我使用ConcurrentBag<T>而不是List<T>这样解决了我的问题:

ConcurrentBag<object> list = new ConcurrentBag<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

2

如果线程从不对列表执行任何其他操作,仅将项目添加到列表中有什么问题吗?

简短的回答:是的。

长答案:运行下面的程序。

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

class Program
{
    readonly List<int> l = new List<int>();
    const int amount = 1000;
    int toFinish = amount;
    readonly AutoResetEvent are = new AutoResetEvent(false);

    static void Main()
    {
        new Program().Run();
    }

    void Run()
    {
        for (int i = 0; i < amount; i++)
            new Thread(AddTol).Start(i);

        are.WaitOne();

        if (l.Count != amount ||
            l.Distinct().Count() != amount ||
            l.Min() < 0 ||
            l.Max() >= amount)
            throw new Exception("omg corrupted data");

        Console.WriteLine("All good");
        Console.ReadKey();
    }

    void AddTol(object o)
    {
        // uncomment to fix
        // lock (l) 
        l.Add((int)o);

        int i = Interlocked.Decrement(ref toFinish);

        if (i == 0)
            are.Set();
    }
}

@royi您是否在单核计算机上运行它?
Bas Smit 2014年

嗨,我认为这个示例存在一个问题,因为它会在发现数字1000时设置AutoResetEvent。因为它可以在希望达到1000之前先达到1000的任何时候都可以处理这些线程。如果您在AddTol方法中添加Console.WriteLine,则会看到编号不正确。
戴夫·沃克

@dave,它在i == 0时设置事件
Bas Smit

2

正如其他人已经说过的那样,您可以使用System.Collections.Concurrent名称空间中的并发集合。如果可以使用其中之一,则首选。

但是,如果您确实想要一个仅同步的列表,则可以查看中的SynchronizedCollection<T>-Class System.Collections.Generic

请注意,您必须包括System.ServiceModel程序集,这也是为什么我不太喜欢它的原因。但是有时候我会用它。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.