Parallel.For(): 在循环外更新变量

38

我只是在了解新的.NET 4.0功能。我正在尝试使用Parallel.For和普通的for(x;x;x)循环进行简单的计算。

然而,我大约50%的时间得到不同的结果。

long sum = 0;

Parallel.For(1, 10000, y =>
    {
        sum += y;
    }
);

Console.WriteLine(sum.ToString());

sum = 0;

for (int y = 1; y < 10000; y++)
{
   sum += y;
}
Console.WriteLine(sum.ToString());

我猜测多个线程同时尝试更新“sum”变量。
有没有明显的方法可以避免这种情况?


4
并发编程分为两部分,一是在独立的线程上执行,二是在线程之间进行同步/通信。并行扩展使得第一部分成为可能,但第二部分必须由开发人员显式地处理。当你说sum += y;时,实际上每个线程都在同时说“把我加入到总和里!”你需要对它们关于共享资源sum的通信进行同步。 - johnny g
24
我的恐惧成真了...并行扩展允许人们在不理解理论的情况下编写并行代码,包括竞争条件(如此例)。 - Andrey
29
@Andrey - 是的,这就是为什么我们中的一些人会先尝试学习它(即通过在 SO 上发布问题)。 - Inisheer
3
@Polaris878 - 和LINQ一样,我也不确定为什么微软要引入它。我们已经有了循环!/讽刺 - Inisheer
4
@Polaris:我们正在走向计算机拥有数百个核心的世界。对于这个世界,将程序变成多线程的是明智之举。此外,你为什么会不喜欢一项让编程变得更容易的技术呢? - BlueRaja - Danny Pflughoeft
显示剩余11条评论
7个回答

71

你不能这样做。 sum 在并行线程之间共享。你需要确保 sum 变量只被一个线程访问:

// DON'T DO THIS!
Parallel.For(0, data.Count, i =>
{
    Interlocked.Add(ref sum, data[i]);
});

但是...这是一种反模式,因为您实际上已经将循环串行化,因为每个线程都会锁定Interlocked.Add

您需要做的是添加子总数并在最后合并它们,如下所示:

Parallel.For<int>(0, result.Count, () => 0, (i, loop, subtotal) =>
    {
        subtotal += result[i];
        return subtotal;
    },
    (x) => Interlocked.Add(ref sum, x)
);

您可以在MSDN上找到更多关于此的讨论:http://msdn.microsoft.com/en-us/library/dd460703.aspx 提示:您可以在《并行编程指南》的第2章中了解更多相关信息。
以下内容也值得一读... 《并行编程模式:理解和应用.NET Framework 4中的并行模式》 - Stephen Toub

1
我在哪里可以找到你在这个答案中使用的重载的确切解释? - Alex Bagnolini
@Alex。您可以在此处找到更多讨论:http://msdn.microsoft.com/en-us/library/dd460703.aspx。我已经使用相同的链接更新了答案。 - Ade Miller

18

sum += y; 实际上等价于 sum = sum + y;。由于以下竞争条件,您可能会得到错误的结果:

  1. 线程1读取sum
  2. 线程2读取sum
  3. 线程1计算sum+y1,并将结果存储在sum
  4. 线程2计算sum+y2,并将结果存储在sum

sum现在等于sum+y2,而不是sum+y1+y2


5

你的推测是正确的。

当你写sum += y时,运行时会执行以下操作:

  1. 将字段读入堆栈
  2. y添加到堆栈中
  3. 将结果写回字段

如果两个线程同时读取字段,则第一个线程所做的更改将被第二个线程覆盖。

你需要使用Interlocked.Add,它将执行加法作为单个原子操作。


5
使用Interlocked.Add的天真方式只会使你的循环变得串行化。 - Ade Miller
我想补充一下,最好的方法是使用本地变量,在循环结束后将它们添加到单个全局变量中,当然要使用Interlocked.Add。 - Andrey
1
下面是我的答案中的示例。 - Ade Miller
此外,不要谈论“将字段读入堆栈”。对于这样的代码,几乎肯定不会发生这种情况(实际上它几乎肯定会在寄存器中)。是否发生这种情况是一项实现细节。虽然 IL 几乎用堆栈来处理所有内容,但从 IL 生成的 x86 代码绝对不会使用堆栈 :) - Luaan
@Luaan:我正在描述IL级别上的操作。 - SLaks
显示剩余2条评论

4

我认为重要的是要区分这个循环不能被划分为并行,因为正如上面提到的,每个循环迭代都依赖于之前的迭代。并行循环适用于显式并行任务,例如像像素缩放等,因为循环的每次迭代不能在其迭代之外具有数据依赖性。

Parallel.For(0, input.length, x =>
{
    output[x] = input[x] * scalingFactor;
});

以上是一段代码示例,它可以轻松地进行分区并实现并行处理。但需要警告的是,并行处理是有代价的,即使上面我用作例子的循环也远远太简单,无需使用并行循环,因为设置时间比通过并行处理节省的时间更长。


你可以将其分区以实现并行处理,只需要从聚合的角度来思考。 - Ade Miller
真实的...MPI_AllGather()是一个很好的例子,但是在MSDN上进行一些初步的研究表明,您需要转向MPI#才能获得该功能...因为它似乎没有包含在内。不过,您可以编写自己的代码来实现。 - Mgetz

4

将long增加并不是一个原子操作。


好观点,SLaks。@TSS:这里有两个操作,加法和保存值 - 你确实需要锁定。 - Eric Mickelsen

3
一个重要的点似乎没有人提到:对于数据并行操作(例如OP的操作),通常更好(在效率和简单性方面)使用PLINQ而不是Parallel类。 OP的代码实际上非常容易并行化:
long sum = Enumerable.Range(1, 10000).AsParallel().Sum();

上述代码段使用了ParallelEnumerable.Sum方法,不过也可以在更一般的情况下使用Aggregate方法。请参考Parallel Loops章节,了解这些方法的说明。请注意不要改动HTML标签。

-1
如果这段代码有两个参数。 例如。
long sum1 = 0;
long sum2 = 0;

Parallel.For(1, 10000, y =>
    {
        sum1 += y;
        sum2=sum1*y;
    }
);

我们该怎么办?我猜我们得使用数组!


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