IEnumerable Linq方法是线程安全的吗?

时间:2012-06-19 15:01:10

标签: c# multithreading thread-safety atomic

我想知道Linq扩展方法是否是原子的?或者,在进行任何类型的迭代之前,我是否需要lock跨线程使用的任何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-将变量声明为volatile:

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

3 个答案:

答案 0 :(得分:21)

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

  

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

     

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

Linq没有改变任何一个。

显然可以使用锁定来同步对象的访问。您必须在访问它的任何地方锁定对象,而不仅仅是在迭代它时。

将集合声明为volatile将不会产生任何积极影响。它只会在读取之前和写入对集合的引用之后产生内存屏障。它不会同步收集阅读或写作。

答案 1 :(得分:9)

简而言之,如上所述,它们不是线程安全的。

但是,这并不意味着您必须在“每种迭代”之前锁定。

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

如果您只是同时对集合执行读取操作,则不需要锁定。 (所以运行像Average,Contains,ElementAtOrDefault这样的LINQ命令就可以了)

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

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

答案 2 :(得分:-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; }
}