【问题标题】:Are IEnumerable Linq methods thread-safe?IEnumerable Linq 方法是线程安全的吗?
【发布时间】:2012-06-19 15:01:09
【问题描述】:

我想知道 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

【问题讨论】:

标签: c# multithreading thread-safety atomic


【解决方案1】:

接口IEnumerable&lt;T&gt; 不是线程安全的。请参阅http://msdn.microsoft.com/en-us/library/s793z9y2.aspx 上的文档,其中指出:

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

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

Linq 不会改变这一切。

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

    【讨论】:

      【解决方案3】:

      这里有一个例子证明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; }
      }
      

      【讨论】:

      • 这并不能证明IEnumerable&lt;T&gt; 扩展方法不是线程安全的。即使Where 运算符是线程安全的,您的程序也会有相同的行为。你不改变集合(ages),你改变一个独立的对象(person)。即使对于并发集合(例如 ConcurrentDictionary&lt;K,V&gt;),这也不是受支持的场景。并发集合的线程安全保证仅限于其内部状态的完整性,而不是它们包含的对象的状态,或其他独立对象的状态。
      • @TheodorZoulias 你有文档支持吗?我并不是说你错了......但是,从我的角度来看,这里存在一个线程安全问题,无论它是否与底层集合有关。您会将我的代码示例中的问题分类为什么?这个概念证明是我们在代码中发现的一个意外错误的结果,我想发布这个让其他人受益。
      • 例如你可以查看ConcurrentDictionary&lt;TKey,TValue&gt;.AddOrUpdate方法的文档:"如果你在不同的线程上同时调用AddOrUpdateaddValueFactory可能会被调用多次。" 所以你可以创建一个类似的程序来证明ConcurrentDictionary 不是线程安全的。但我们知道它是,所以证明是假的。
      猜你喜欢
      • 2014-08-21
      • 1970-01-01
      • 2012-11-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-31
      相关资源
      最近更新 更多