在 parallel.foreach 循环中捕获的变量

Posted

技术标签:

【中文标题】在 parallel.foreach 循环中捕获的变量【英文标题】:captured variables in a parallel.foreach loop 【发布时间】:2015-03-04 18:04:43 【问题描述】:

每个线程是否都有自己可以更改的副本?他们都共享同一个吗?它是线程安全的吗?

X var;
Parallel.Foreach(ls , it => Op(var, It));

Op 对 var 做了一些事情。我假设如果 var 是一个引用(比如List<string>),那么所有线程都共享一个引用,我的工作就是练习安全更新。如果 X 是 int say 呢?

如果 var 是 List<string> 但为空怎么办?如果一个线程创建一个新列表并将其写入var?其他线程看到 var 的新值了吗?

我已经做了一些测试,但我想确保我看到的是真实的,而不是我在编写糟糕的测试。

最后一种情况(null)似乎每个线程都有自己的List<string>

编辑: 看来我需要区分

   X var;
    Parallel.Foreach(ls , it => Op(var, It));

   X var;
    Parallel.Foreach(ls , it => 
          ....
          var = <something>
          ....
      );

即 lambda 本身会修改变量。在这种情况下,它是共享的。但是在函数(Op)的情况下,关于按值传递的通常规则适用

在共享 var 的情况下,任务并行库是管理并发访问还是我必须这样做?

【问题讨论】:

【参考方案1】:

你在Op中通过值传递var,所以Op不能改变var,因为Op只有它的副本。

编辑:

即 lambda 本身会修改变量。在这种情况下,它是共享的。但是在函数(Op)的情况下,关于按值传递的通常规则适用

是的,如果你想共享变量,那么你应该通过 lambda 本身来引用它,或者通过引用将它传递给 OpOp(ref var,it),假设 Op 的签名也发生了变化),而不是通过值。

在共享 var 的情况下,任务并行库是管理并发访问还是我必须这样做?

你必须自己管理并发访问。

【讨论】:

这只是说他的特定实现碰巧永远不会改变变量,而不是它不能。 lambda(以及因此,从每个线程访问的代码)正在关闭变量,这意味着每个线程都可以对其进行变异并让其他线程观察到这些变异。 @Servy Op 不能改变变量,因为它看不到变量,它只看到值。 没错,但是线程,即通过 lambda 运行的代码,确实 看到了变量并且可以 改变它。同样,所示 lambda 的特定实现 不会 改变变量这一事实并不意味着它不能 @Servy 我没有说“lambda 不能改变var”,我说“Op 不能改变var 是的,我很清楚这一点,但问题只是询问Op 的实现。它询问是否所有线程都可以访问同一个变量,以及对它的突变是否在它们之间可见。它们都共享同一个变量,并且它们之间的变化可见。所示的实现不涉及它们的变异并不会改变这一点。您提供了事实正确的信息,但未能回答问题。你的答案也可以说是 1 + 1 = 2,这是正确的,但仍然没有回答问题。【参考方案2】:

仅复制变量引用。不是可变数据。就好像所有的局部变量都通过方法调用传递给匿名委托一样。

每MSDN (Anonymous Methods (C# Programming Guide))

当外部变量 n 的引用被捕获时 委托已创建。与局部变量不同, 捕获的变量一直延伸到引用该变量的委托 匿名方法有资格进行垃圾回收。

【讨论】:

引用甚至没有被复制; lambda 的所有调用都将访问相同的变量 同一个对象。在代码中,对象由变量命名。如果调用者更改了对象的属性,匿名方法将看到这些更改。 是的,这是真的。如果其中一个调用改变了变量,而不是它引用的值(如果它甚至是引用类型),那么其他调用也将观察到这种变化,因为它们都共享相同的变量,而不是复制它。闭包关闭 variable,而不是 values【参考方案3】:

该 lambda 的所有调用都将访问同一个变量。每次调用不会有单独的副本。

编译器将把闭包转换成与以下内容等效的东西:

public class ClosureClass

    public X var;
    public void method1(Y it)
    
        Op(var, it);
    


IEnumerable<Y> ls = null;
ClosureClass closure = new ClosureClass();
closure.var = null;
Parallel.ForEach(ls, closure.method1);

将创建一个类来表示闭包,将在方法的开头创建一个实例,lambda 的主体将映射到闭包类中的一个方法,并且所有封闭变量的使用都将是其中的字段班级。正如您在此处看到的,closure.method1 的所有调用最终都将访问同一实例的同一字段,这是一个单一变量。

【讨论】:

因此 Op(null, it) 与 var = null 的 Op(var, it) 不同?在第一种情况下,没有捕获任何内容,每个线程都看到自己的 'null' ,在第二种情况下,它们都共享相同的 var @pm100 是的,如果你不关闭变量而是内联一个值,你将没有闭包语义。

以上是关于在 parallel.foreach 循环中捕获的变量的主要内容,如果未能解决你的问题,请参考以下文章

在 parallel.ForEach 循环中获取线程 ID

我在 Parallel.ForEach 循环中收到 TaskCanceledException,如何解决?

如何正确调用 Parallel.ForEach 循环中的调用异步方法[重复]

Stringbuilder用于Parallel.Foreach循环

为啥覆盖 Parallel.foreach 循环的 .NET 单元测试依赖于硬件?

带有 BlockingCollection.GetConsumableEnumerable 的 Parallel.ForEach 循环