在这一系列文章中,我们将会经常用到下面的这些代码, 这些代码中有计算立方和与平方和的代码,没有什么具体意义,只是为了演示并行运算。
1: class Program
2: {
3: private const int NUM_MAX = 100000000;
4:
5: private static void CubicSum()
6: {
7: double sum = 0;
8: var sw = Stopwatch.StartNew();
9: for (int i = 0; i < NUM_MAX; i++)
10: {
11: sum = sum + Math.Pow((i * (i + 1) / 2), 2);
12: }
13:
14: Console.WriteLine("CubicSum Excuation Time: {0}",sw.Elapsed.ToString());
15: //Console.WriteLine("Cubic Result: {0}", sum);
16: //Debug.WriteLine(sw.Elapsed.ToString());
17: }
18:
19: private static void QuadraticSum()
20: {
21: double sum = 0;
22: var sw = Stopwatch.StartNew();
23: for (int i = 0; i < NUM_MAX; i++)
24: {
25: sum = sum + (2 * i + 1) * (i + 1) * i / 6;
26: }
27:
28: Console.WriteLine("QuadraticSum Excuation Time: {0}", sw.Elapsed.ToString());
29: //Console.WriteLine("Quadratic Result: {0}", sum);
30: //Debug.WriteLine(sw.Elapsed.ToString());
31: }
32:
33: static void Main(string[] args)
34: {
35: var swSequence = Stopwatch.StartNew();
36: QuadraticSum();
37: CubicSum();
38: //Debug.WriteLine(swSequence.Elapsed.ToString());
39: Console.WriteLine("Sequence Excuation Time: {0}", swSequence.Elapsed.ToString());
40:
41: Console.WriteLine("--------------------------------------------------");
42:
43: var swParallel = Stopwatch.StartNew();
44: Parallel.Invoke(() => QuadraticSum(), () => CubicSum());
45: Console.WriteLine("Parallel Excuation Time: {0}", swParallel.Elapsed.ToString());
46: //Debug.WriteLine(swParallel.Elapsed.ToString());
47:
48: Console.WriteLine("Main method finish!");
49: Console.ReadLine();
50: }
51: }
在System.Threading.Tasks.Parallel里提供了以下一些静态方法;我们可以通过下面的这些方法实现并行运算:
I. Parallel.Invoke
并行运行多个方法最简单的方法就是使用这个方法,注意这些方法最好是独立的。先看看并行执行的优势,执行上面的代码,你会看到如下的结果:
第一次看到并行对于性能的提高是不是有些兴奋,不要着急在后续章节中,我们会深入到CPU的每个核中去看看,并且也会讲一下分析并行运算的性能问题。
对于Parallel.Invoke方法有两个重载,第一个接受一个System.Action的数组
第二个增加了一个ParallelOptions (这个我们将在后续章节中讲解)
可以用以下几种方式来使用Invoke
1. 就像它的定义那样,定义一个Action数组,然后传递进去
1: Action[] actionArray = new Action[3];2: actionArray[0] = new Action(() => Console.WriteLine("Action 0"));3: actionArray[1] = new Action(() => Console.WriteLine("Action 1"));4: actionArray[2] = new Action(() => Console.WriteLine("Action 2"));5:
6: Parallel.Invoke(actionArray);
2. 直接把方法名做为参数,逐一传递进去
1: static void Main(string[] args)2: {
3: Parallel.Invoke(Method1, Method2, Method3);
4:
5: Console.WriteLine("Main method finish!");6: Console.ReadLine();
7: }
8:
9: static void Method1()10: {
11: Console.WriteLine("Method 1");12: }
13:
14: static void Method2()15: {
16: Console.WriteLine("Method 2");17: }
18:
19: static void Method3()20: {
21: Console.WriteLine("Method 3");22: }
3. 当然你可以使用Lambda表达式、匿名函数和匿名委托
1: static void Main(string[] args)2: {
3: Parallel.Invoke(
4: () => { Console.WriteLine("Method 1"); },5: () => { Console.WriteLine("Method 2"); },6: delegate() { Console.WriteLine("Method 3"); }7: );
8:
9: Console.WriteLine("Main method finish!");10: Console.ReadLine();
11: }
Invoke的缺点:
1. 使用Invoke时,它所调用的方法是以一种不确定的先后顺序执行的,大家可以多次尝试上述代码,看看执行效果就知道了。因此对于一些比较复杂的并行算法,比如一些需要一个执行计划来控制并发方法的并行算法,简单的Invoke就不适用。
2. 当它所调用的方法之间,运行时间差异很大的时候,它会运行所需的时间将会大于等于运行时间最长的那个方法所需的时间。因此导致CPU中的一些核长时间处于空闲状态。
3. 因为它始终调用固定数量的方法,因此在一个多核的环境中,它可能仅仅使用其中的某几个核,而导致其他核处于空闲状态,比如把上述代码放到一个8核或者更多核的环境中,其中的几个核就会处于空闲状态。
4. 如果它调用的方法之间,存在相互依赖,就会引起很难查找的并发问题,比如在开篇中提到的死锁;当然这个问题不仅仅是Invoke才会存在的问题。
5. 如果在Invoke中抛出Exception,比通常的顺序执行抛出的异常要复杂一些, 所有调用的方法抛出的异常都会被打包放在一个System.AggregateException中,关于这一点我们会在后续文章中讲解到。
Invoke的优点:
1. 使用起来简单,不用担心那些Tasks或者Threads的问题。