IEnumerable Linq方法是否线程安全?

24
我想知道Linq扩展方法是否是原子性的?或者在任何迭代之前,我是否需要锁定跨线程使用的任何IEnumerable对象?
声明变量为volatile是否对此有任何影响?
总之,以下哪种操作是最好的、线程安全的操作?
1-没有任何锁:
IEnumerable<T> _objs = //...
var foo = _objs.FirstOrDefault(t => // some condition

2- 包括锁定语句:

IEnumerable<T> _objs = //...
lock(_objs)
{
    var foo = _objs.FirstOrDefault(t => // some condition
}

3- Declaring variable as volatile:

volatile IEnumerable<T> _objs = //...
var foo = _objs.FirstOrDefault(t => // some condition

它们不是线程安全的。请参见https://dev59.com/MWkw5IYBdhLWcg3wSovA - stuartd
3个回答

26
接口 IEnumerable<T> 不是线程安全的。参见 http://msdn.microsoft.com/en-us/library/s793z9y2.aspx 的文档,其中说明:

只要集合保持不变,枚举器将保持有效。如果对集合进行更改,例如添加、修改或删除元素,则枚举器将无法恢复地失效,并且其行为是未定义的。

枚举器并没有独占访问集合的权限;因此,遍历集合本质上不是一个线程安全的过程。为了在枚举期间保证线程安全,可以在整个枚举期间锁定集合。为了允许多个线程对集合进行读写访问,必须实现自己的同步。

Linq 对此没有任何改变。

显然可以使用锁定来同步访问对象。不过你必须在访问它的每个位置上都进行锁定,而不仅是在遍历时。

声明集合为 volatile 将不会产生任何积极影响。这只会导致在对集合引用进行读取和写入之前进行内存屏障,但不会同步集合的读取或写入。


10

简而言之,正如上面所提到的,它们不是线程安全的。

然而,这并不意味着您必须在“每种迭代”之前进行锁定。

您需要将更改集合(添加、修改或删除元素)的所有操作与执行其他操作(添加、修改、删除元素或读取元素)同步。

如果您只对集合进行并发读取操作,则不需要加锁。(因此同时运行LINQ命令,如Average、Contains、ElementAtOrDefault等都可以)

如果集合中的元素为机器字长,例如大多数32位计算机上的Int,则修改该元素的值已经具有原子性。在这种情况下,不要添加或删除集合中的元素而不加锁,但是修改值可能是可以接受的,如果您可以处理设计中的某些非确定性。

最后,您可以考虑对集合的单个元素或部分进行细粒度锁定,而不是锁定整个集合。


-1
这是一个例子,证明了 IEnumerable 扩展方法不是线程安全的。在我的机器上,throw new Exception("BOOM"); 这一行总是在几秒钟内触发。
希望我已经足够详细地记录了代码,以解释如何触发线程问题。
您可以在 linqpad 中运行此代码,亲自查看。
async Task Main()
{
    // The theory is that it will take a longer time to query a lot of items
    // so there should be a better chance that we'll trigger the problem. 
    var listSize = 999999;
    
    // Specifies how many tasks to spin up. This doesn't necessarily mean
    // that it'll spin up the same number of threads, as we're using the thread
    // pool to manage that stuff. 
    var taskCount = 9999;

    // We need a list of things to query, but the example here is a bit contrived. 
    // I'm only calling it `ages` to have a somewhat meaningful variable name. 
    // This is a distinct list of ints, so, ideally, a filter like:
    // `ages.Where(p => p == 4` should only return one result. 
    // As we'll see below, that's not always the case. 
    var ages = Enumerable
        .Range(0, listSize)
        .ToList();
    
    // We'll use `rand` to find a random age in the list. 
    var rand = new Random();
    
    // We need a reference object to prove that `.Where(...)` below isn't thread safe. 
    // Each thread is going to modify this shared `person` property in parallel. 
    var person = new Person();
    
    // Start a bunch of tasks that we'll wait on later. This will run as parallel
    // as your machine will allow. 
    var tasks = Enumerable
        .Range(0, taskCount)
        .Select(p => Task.Run(() =>
        {
            // Pick a random age from the list. 
            var age = ages[rand.Next(0, listSize)];
            
            // These next two lines are where the problem exists. 
            // We've got multiple threads changing `person.Age` and querying on `person.Age` 
            // at the same time. As one thread is looping through the `ages` collection
            // looking for the `person.Age` value that we're setting here, some other
            // thread is going to modify `person.Age`. And every so often, that will
            // cause the `.Where(...)` clause to find multiple values. 
            person.Age = age;
            var count = ages.Where(a => a == person.Age).Count();

            // Throw an exception if the `.Where(...)` filter returned more than one age. 
            if (count > 1) {
                throw new Exception("BOOM");
            }
        }));
        
    await Task.WhenAll(tasks);
    
    Console.WriteLine("Done");
}

class Person {
    public int Age { get; set; }
}

2
这并不证明 IEnumerable<T> 扩展方法是线程安全的。即使 Where 操作符是线程安全的,您的程序仍将具有相同的行为。您没有改变集合(ages),而是改变了一个独立的对象(person)。即使对于像 ConcurrentDictionary<K,V> 这样的并发集合,这也不是受支持的场景。并发集合的线程安全保证仅限于其内部状态的完整性,而不是它们包含的对象或其他独立对象的状态。 - Theodor Zoulias
@TheodorZoulias,你有支持这个说法的文档吗?我不是说你错了...但从我的角度来看,无论它是否与底层集合相关,都存在线程安全问题。你会如何分类我的代码示例中的问题?这个概念验证是我们在代码中发现的一个意外错误的结果,我想将其发布出来让其他人受益。 - Alex Dresko
作为示例,您可以查看 ConcurrentDictionary<TKey,TValue>.AddOrUpdate 方法的文档:“如果您在不同的线程上同时调用 AddOrUpdate,则可能会多次调用 addValueFactory。” 因此,您可以创建一个类似的程序来证明 ConcurrentDictionary 不是线程安全的。但我们知道它是线程安全的,所以这个证明是虚假的。 - Theodor Zoulias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接