Parallel并行计算合计数据时错误的原因和解决办法

在.NET 4.0中System.Threading.Tasks.Parallel类中有几个很方便调用的并行计算方法,我想各位会经常用到Parallel.For,Parallel.ForEach,正常来说我们调用它然后传递一个独立的方法进去后它可以很好的并行计算,为我节约很多时间,很好的利用CPU的计算能力,但是在一些特别的情况下,却得不到我们所希望的结果,下面简单的说一下在具体的项目中遇到的问题。

在项目中我需要调用一个静态的计算类,这个计算类会去查数据库,然后得到一个数据集进行计算。

为了简单起见,我做了一个简单的计算的数据类,用线程等待来模拟数据库读取中的延时:

   1: class Calc
   2: {
   3:     private static Calc _instance = new Calc();
   4:  
   5:     private Calc() { }
   6:  
   7:     public static Calc Instance { get { return _instance; } }
   8:  
   9:     public double Add(double x, double y)
  10:     {
  11:         Thread.Sleep(50);
  12:         return x + y;
  13:     }
  14: }

为了保持这个例子的简单性,这里的计算方法很简单,它是一个单件类,ADD方法计算两数相加,在前面会让程序停一下以便有一种读取数据库延时的效果。

这个计算的类大家已经明白了,现在看一下调用的时候的情况吧,这里的需求也很简单,就是循环100次,让他们累加最后得到合计数。我们应用程序中也经常会使用这种情况,比如从数据库中读取100条记录,然后每一条经过一个业务逻辑处理,然后累加计算的值。平时我会这么写:

   1: double total = 0;
   2:  
   3: for (int i = 0; i < 100; i++)
   4:     total += Calc.Instance.Add(i, i + 1);
   5:  
   6: Console.WriteLine("使用for顺序计算:{0}", total);

我想大多数的朋友也会这么写,在单核的时候这样无可厚非,最后我们得到total就是计算结果。

那现在我们试着用.NET4.0的Parallel类的并行计算来优化一下这段代码吧,简单的将for换成Parallel.For,得到下面的这段代码:

   1: double total = 0;
   2:  
   3: Parallel.For(0, 100, 
   4:     (i) => total += Calc.Instance.Add(i, i + 1));
   5:  
   6: Console.WriteLine("使用ParallelFor并行计算:{0}", total);

看起来万事大吉了,但是别高兴的太高,把上面两段代码进行后就会发现,其实结果是不一样的,也就是说我们加入了并行计算后得到的total是错误的。(这就是这篇文章要探讨的问题)

为什么我们换上后得到的计算结果会和顺序执行结果不同呢?原因就在于Parallel的数据共享问题,园子里Henry Cui已经详细研究了Parallel的数据共享这个问题,.Net 4.0 Parallel 编程(七)Task中的数据共享,在这里我就不再重复了,他最终探讨的问题是任务间并行计算的时候数据是被共享的,而不是我们平时用for的时候所理解的循环中的数据是私有的,那么我们在Parallel.For中应该怎么样做才能得到正确的结果呢?看一下如下的代码:

   1: double total = 0;
   2:  
   3: Parallel.For<double>(0, 100, () => 0, (j, loop, localFinallyTotal) =>
   4: {
   5:     localFinallyTotal += Calc.Instance.Add(j, j + 1);
   6:     return localFinallyTotal;
   7: }, (localFinally) => total += localFinally);
   8:  
   9: Console.WriteLine("使用线程局部变量ParallelFor并行计算:{0}", total);

在这里我加入了线程本地变量,在例子中localFinallyTotal的有效区域则是在当前线程中,最后的total+=localFinally则是将所有线程中的临时结果都合计起来,这样便可以得到一个正确并行计算的结果。

为了测试这样执行的性能,我使用了CodeTimer类,该类是从老赵的一个简单的性能计数器:CodeTimer里得到的。运行结果如下图:

image

从结果上可以看得出执行的时间是一样的,并没有因为增加了本地变量而增加多少执行时间,但是CPU的周期却增加了很多,这说明CPU工作量增大了。

参照MSDN中关于关行计算的相关内容:http://msdn.microsoft.com/zh-cn/library/dd537608.aspx

原文地址:https://www.cnblogs.com/biyusoft/p/3432055.html