(转)Net4.0 Parallel编程 Data Parallelism
Thread-Local Variables
首先我们来看下线程局部变量,是的我们也许一直在想我们如何去定义一个线程局部变量呢。先看段顺序执行的代码:
01.[TestMethod()] 02.public void NormalSequenceTest() 03.{ 04. int[] nums = Enumerable.Range(0, 1000000).ToArray(); 05. long total = 0; 06. for (int i = 0; i < nums.Length;i++ ) 07. { 08. total += nums[i]; 09. } 10. Console.WriteLine("The total is {0}", total); 11.}执行结果:
我们再来看这段代码:
01.[TestMethod()] 02.public void NormalParallelTest() 03.{ 04. int[] nums = Enumerable.Range(0, 1000000).ToArray(); 05. long total = 0; 06. Parallel.For(0,nums.Length,i=> 07. { 08. total += nums[i]; 09. }); 10. Console.WriteLine("The total is {0}", total); 11.}执行结果:
再运行下:
也许我们会感到很奇怪为什么会这样呢,其实我们想想就可以明白了,total变量是公共的,而我们的程序是多个线程的加,而多个线程之间是不能把数据共享的。其实我们需要的是在每个线程中计算出一个和值,然后再进行累加。我们来看看线程局部变量:
01.[TestMethod()] 02.public void ThreadLocalTest() 03.{ 04. int[] nums = Enumerable.Range(0, 1000000).ToArray(); 05. long total = 0; 06. Parallel.For<long>(0, nums.Length, () => 0, (j, loop, subtotal) => 07. { 08. subtotal += nums[j]; 09. return subtotal; 10. }, 11. (x) => Interlocked.Add(ref total, x) 12. ); 13. 14. Console.WriteLine("The total is {0}", total); 15.}我们再看下执行结果:
下面说下泛型方法Parallel.For<T>方法,方法的原型:
1.public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);TLocal:线程变量的类型;第一个、第二个参数就不必多说了,就是其实值跟结束值。
localInit:每个线程的线程局部变量初始值的设置;
body:每次循环执行的方法,其中方法的最后一个参数就是线程局部变量;
localFinally:每个线程之后执行的方法。
相信这样解释后就能明白了为什么需要线程局部变量了,也明白如何使用线程局部变量了。我们再看看在Parallel.Foreach<T>中如何使用:
01.[TestMethod()] 02.public void ForeachThreadLocalTest() 03.{ 04. int[] nums = Enumerable.Range(0, 1000000).ToArray(); 05. long total = 0; 06. Parallel.ForEach<int,long>(nums,()=>0,(member,loopState,subTotal)=> 07. { 08. subTotal += member; 09. return subTotal; 10. }, 11. (perLocal)=> Interlocked.Add(ref total,perLocal) 12. ); 13. Console.WriteLine("The total is {0}", total); 14.}要注意的是,我们必须要使用ForEach<TSource, TLocal>,因为第一个参数表示的是迭代源的类型,第二个表示的是线程局部变量的类型,其方法的参数跟For是差不多的。
Break、Stop
首先我们可以看到在Parallel.For的一个重载方法中:
1.public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int, ParallelLoopState> body);在委托的最后一个参数类型为ParallelLoopState,而ParallelLoopState里面提供给我们两个方法:Break、Stop来终止迭代,而Break跟Stop的区别是什么呢?我们来看两段代码:
01.private void StopLoop() 02.{ 03. var Stack = new ConcurrentStack<string>(); 04. Parallel.For(0, 10000, (i, loopState) => 05. { 06. if (i < 1000) 07. Stack.Push(i.ToString()); 08. else09. { 10. loopState.Stop(); 11. return; 12. } 13. }); 14. Console.WriteLine("Stop Loop Info:\n elements count:{0}", Stack.Count); 15.} 16.private void BreakLoop() 17.{ 18. var Stack = new ConcurrentStack<string>(); 19. var stringList = this.ConstructString(10000); 20. Parallel.For(0, stringList.Count, (i, loopState) => 21. { 22. Stack.Push(stringList[i]); 23. if (stringList[i].Contains("999")) 24. { 25. loopState.Break(); 26. } 27. }); 28. Console.WriteLine("Stop Loop Info:\n elements count:{0}", Stack.Count); 29.} 30.private List<string> ConstructString(int number) 31.{ 32. var stringList = new List<string>(); 33. Parallel.For(0, number - 1, i => 34. { 35. stringList.Add(i.ToString()); 36. }); 37. return stringList; 38.}测试方法:
1.[TestMethod()] 2.public void LoopTest() 3.{ 4. StopLoop(); 5. BreakLoop(); 6.}来看看运行结果吧:
其实这个例子只是想告诉大家,为什么第一个用Stop,第二个用Break。原因是:第一个例子中我只关心的是循环的迭代变量i的值,我只要1000个字符串,而不去管这1000个字符串是什么东西。所以当达到1000时,我们就立刻停止所有的迭代包括其他线程上的。而第二个方法中我们是判断的源中的某个索引值,这个时候有可能较早的元素还未处理。
其实在我们调用过Stop或者Break方法后,循环上的其他的线程可能还会运行一段时间,其实我们可以通过IsStopped属性来判断循环是在其他线程上停止。Foreach中的使用就不再看了,跟For是一样的。
总结
在本文中,主要介绍了如何停止循环、使用线程局部变量。在里面我们看到了我们在使用并行开发时,有很多东西是需要我们去注意的。在下文中将介绍下异常处理、取消循环等话题。
在本文中介绍如何在外部去取消循环、以及异常的处理。
Cancel
在并行的循环中支持通过传递ParallelOptions参数中的CancellationToken进行取消循环的控制,我们可以CancellationTokenSource实例化之后传递给ParallelOptions对象Cancellation值。下面来看个示例:
01.[TestMethod] 02.public void CancelLoop() 03.{ 04. var sourceNums = Enumerable.Range(0, 1000000000); 05. var cts = new CancellationTokenSource(); 06. var po = new ParallelOptions(); 07. var stack = new ConcurrentStack<int>(); 08. po.CancellationToken = cts.Token; 09. po.MaxDegreeOfParallelism = System.Environment.ProcessorCount; 10. Task.Factory.StartNew(() => 11. { 12. foreach (var num in sourceNums) 13. { 14. if (num == 1000000) 15. cts.Cancel(); 16. } 17. }); 18. try19. { 20. Parallel.ForEach(sourceNums,po, num => 21. { 22. stack.Push(num); 23. po.CancellationToken.ThrowIfCancellationRequested(); 24. }); 25. } 26. catch (OperationCanceledException e) 27. { 28. Console.WriteLine(e.Message); 29. } 30. Console.WriteLine(stack.Count); 31.}我们来看下运行的结果:
解释下上面的方法,并行循环的意图是将sourceNums里面的元素推到Stack中,然后另外开启了一个线程来控制了什么时候进行cancel操作。也许会有个疑问,为什么不是1000000呢,原因很简单就是上面的控制的线程不可能跟下面的同时开始的,而其每次迭代运行所需要的时间也是不同的。
上面的示例中我们看的是如何终止Parallel的ForEach循环,终止For循环是一样的,For方法中也提供了ParallelOptions参数。
Handel Exceptions
在处理并行循环的异常的与顺序循环异常的处理是有所不同的,并行循环里面可能会一个异常在多个循环中出现,或则一个线程上的异常导致另外一个线程上也出现异常。比较好的处理方式就是,首先获取所有的异常最后通过AggregateException来包装所有的循环的异常,循环结束后进行throw。看一段示例代码:
01.private void HandleNumbers(int[] numbers) 02.{ 03. var exceptions = new ConcurrentQueue<Exception>(); 04. Parallel.For(0, numbers.Length, i => 05. { 06. try07. { 08. if (numbers[i] > 10 && numbers[i] < 20) 09. { 10. throw new Exception(String.Format("numbers[{0}] betwewn 10 to 20",i)); 11. } 12. } 13. catch (Exception e) 14. { 15. exceptions.Enqueue(e); 16. } 17. }); 18. if (exceptions.Count > 0) 19. throw new AggregateException(exceptions); 20.}测试方法:
01.[TestMethod()] 02.public void HandleExceptions() 03.{ 04. var numbers = Enumerable.Range(0, 10000).ToArray(); 05. try06. { 07. this.HandleNumbers(numbers); 08. } 09. catch(AggregateException exceptions) 10. { 11. foreach (var ex in exceptions.InnerExceptions) 12. { 13. Console.WriteLine(ex.Message); 14. } 15. } 16.}
测试结果:
对上面的方法说明下,在HandleNumbers方法中,就是一个小的demo如果元素的值出现在10-20之间就抛出异常。在上面我们的处理方法就是:在循环时通过队列将所有的异常都集中起来,循环结束后来抛出一个AggregateException。
总结
在本文中主要说明了如何处理异常以及如何在外部取消一个并行循环。到此Task Parallel Library中的数据并行部分已经结束。下面的会就学习下,任务并行部分。
出处: http://henllyee.cnblogs.com/
浙公网安备 33010602011771号