1. 程式人生 > >C#並行-第一天-Parallel

C#並行-第一天-Parallel

隨著多核時代的到來,並行開發越來越展示出它的強大威力!使用並行程式,充分的利用系統資源,提高程式的效能。在.net 4.0中,微軟給我們提供了一個新的名稱空間:System.Threading.Tasks。這裡面有很多關於並行開發的東西,今天第一篇就介紹下最基礎,最簡單的——認識和使用Parallel。

  一、 Parallel的使用

在Parallel下面有三個常用的方法invoke,For和ForEach。

1、  Parallel.Invoke

這是最簡單,最簡潔的將序列的程式碼並行化。

在這裡先講一個知識點,就是StopWatch的使用,最近有一些人說找不到StopWatch,StopWatch到底是什麼東西,今天就來說明一下。

StopWatch在System.Diagnostics命名控制元件,要使用它就要先引用這個名稱空間。

其使用方法如下:

var stopWatch = new StopWatch();   //建立一個Stopwatch例項

stopWatch.Start();   //開始計時

stopWatch.Stop();   //停止計時

stopWatch.Reset();  //重置StopWatch

stopWatch.Restart(); //重新啟動被停止的StopWatch

stopWatch.ElapsedMilliseconds //獲取stopWatch從開始到現在的時間差,單位是毫秒

本次用到的就這麼多知識點,想了解更多關於StopWatch的,去百度一下吧,網上有很多資料。

下面進入整體,開始介紹Parallel.Invoke方法,廢話不多說了,首先新建一個控制檯程式,新增一個類,程式碼如下:

複製程式碼
 public class ParallelDemo
      {
         private Stopwatch stopWatch = new Stopwatch();
     </span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> Run1()
     {
        Thread.Sleep(</span><span style="color:#800080;">2000</span><span style="color:#000000;">);
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Task 1 is cost 2 sec</span><span style="color:#800000;">"</span><span style="color:#000000;">);
     }
     </span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> Run2()
     {
        Thread.Sleep(</span><span style="color:#800080;">3000</span><span style="color:#000000;">);
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Task 2 is cost 3 sec</span><span style="color:#800000;">"</span><span style="color:#000000;">);
     }

     </span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> ParallelInvokeMethod()
     {
        stopWatch.Start();</span><span style="color:#000000;">
        Parallel.Invoke(Run1, Run2);</span><span style="color:#000000;">
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Parallel run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);
        
        stopWatch.Restart();
        Run1();
        Run2();
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Normal run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);
     }

複製程式碼

程式碼很簡單,首先新加一個類,在類中寫了兩個方法,Run1和Run2,分別等待一定時間,輸出一條資訊,然後寫了一個測試方法ParallelInvokeMethod,分別用兩種方法呼叫Run1和Run2,然後在main方法中呼叫,下面來看一下執行時間如何:

  大家應該能夠猜到,正常呼叫的話應該是5秒多,而Parallel.Invoke方法呼叫用了只有3秒,也就是耗時最長的那個方法,可以看出方法是並行執行的,執行效率提高了很多。

2、Parallel.For

這個方法和For迴圈的功能相似,下面就在類中新增一個方法來測試一下吧。程式碼如下:

複製程式碼
public void ParallelForMethod()
     {
            stopWatch.Start();
            for (int i = 0; i < 10000; i++)
            {
               for (int j = 0; j < 60000; j++)
               {
                  int sum = 0;
                  sum += i;
               }
            }
            stopWatch.Stop();
            Console.WriteLine("NormalFor run " + stopWatch.ElapsedMilliseconds + " ms.");
        stopWatch.Reset();
        stopWatch.Start();
        Parallel.For(</span><span style="color:#800080;">0</span>, <span style="color:#800080;">10000</span>, item =&gt;<span style="color:#000000;">
        {</span>
           <span style="color:#0000ff;">for</span> (<span style="color:#0000ff;">int</span> j = <span style="color:#800080;">0</span>; j &lt; <span style="color:#800080;">60000</span>; j++<span style="color:#000000;">)
           {
              </span><span style="color:#0000ff;">int</span> sum = <span style="color:#800080;">0</span><span style="color:#000000;">;
              sum </span>+=<span style="color:#000000;"> item;
           }
        });
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">ParallelFor run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);
       
 }</span></pre>
複製程式碼

寫了兩個迴圈,做了一些沒有意義的事情,目的主要是為了消耗CPU時間,同理在main方法中呼叫,執行結果如下圖:

可以看到,Parallel.For所用的時間比單純的for快了1秒多,可見提升的效能是非常可觀的。那麼,是不是Parallel.For在任何時候都比for要快呢?答案當然是“不是”,要不然微軟還留著for幹嘛?

下面修改一下程式碼,新增一個全域性變數num,程式碼如下:

複製程式碼
public void ParallelForMethod()
         {
            var obj = new Object();
            long num = 0;
            ConcurrentBag<long> bag = new ConcurrentBag<long>();
        stopWatch.Start();
        </span><span style="color:#0000ff;">for</span> (<span style="color:#0000ff;">int</span> i = <span style="color:#800080;">0</span>; i &lt; <span style="color:#800080;">10000</span>; i++<span style="color:#000000;">)
        {
           </span><span style="color:#0000ff;">for</span> (<span style="color:#0000ff;">int</span> j = <span style="color:#800080;">0</span>; j &lt; <span style="color:#800080;">60000</span>; j++<span style="color:#000000;">)
           {
              </span><span style="color:#008000;">//</span><span style="color:#008000;">int sum = 0;
              </span><span style="color:#008000;">//</span><span style="color:#008000;">sum += item;</span>
              num++<span style="color:#000000;">;
           }
        }
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">NormalFor run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);

        stopWatch.Reset();
        stopWatch.Start();
        Parallel.For(</span><span style="color:#800080;">0</span>, <span style="color:#800080;">10000</span>, item =&gt;<span style="color:#000000;">
        {
           </span><span style="color:#0000ff;">for</span> (<span style="color:#0000ff;">int</span> j = <span style="color:#800080;">0</span>; j &lt; <span style="color:#800080;">60000</span>; j++<span style="color:#000000;">)
           {
              </span><span style="color:#008000;">//</span><span style="color:#008000;">int sum = 0;
              </span><span style="color:#008000;">//</span><span style="color:#008000;">sum += item;</span>
              <span style="color:#0000ff;">lock</span><span style="color:#000000;"> (obj)
              {
                 num</span>++<span style="color:#000000;">;
              }
           }
        });
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">ParallelFor run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);
       
     }</span></pre>
複製程式碼

Parallel.For由於是並行執行的,所以會同時訪問全域性變數num,為了得到正確的結果,要使用lock,此時來看看執行結果:

 

  是不是大吃一驚啊?Parallel.For竟然用了15秒多,而for跟之前的差不多。這主要是由於並行同時訪問全域性變數,會出現資源爭奪,大多數時間消耗在了資源等待上面。

一直說並行,那麼從哪裡可以看出來Parallel.For是並行執行的呢?下面來寫個測試程式碼:

Parallel.For(0, 100, i =>
            {
               Console.Write(i + "\t");
            });

從0輸出到99,執行後會發現輸出的順序不對,用for順序肯定是對的,並行同時執行,所以會出現輸出順序不同的情況。

2、Parallel.Foreach

這個方法跟Foreach方法很相似,想具體瞭解的,可以百度些資料看看,這裡就不多說了,下面給出其使用方法:

List<int> list = new List<int>();
            list.Add(0);
            Parallel.ForEach(list, item =>
            {
               DoWork(item);
            });
 二、 Parallel中途退出迴圈和異常處理

1、當我們使用到Parallel,必然是處理一些比較耗時的操作,當然也很耗CPU和記憶體,如果我們中途向停止,怎麼辦呢?

在序列程式碼中我們break一下就搞定了,但是並行就不是這麼簡單了,不過沒關係,在並行迴圈的委託引數中提供了一個ParallelLoopState,

該例項提供了Break和Stop方法來幫我們實現。

Break: 當然這個是通知平行計算儘快的退出迴圈,比如平行計算正在迭代100,那麼break後程序還會迭代所有小於100的。

Stop:這個就不一樣了,比如正在迭代100突然遇到stop,那它啥也不管了,直接退出。

下面來寫一段程式碼測試一下:

複製程式碼
public void ParallelBreak()
         {
            ConcurrentBag<int> bag = new ConcurrentBag<int>();
            stopWatch.Start();
            Parallel.For(0, 1000, (i, state) =>
            {
               if (bag.Count == 300)
               {
                  state.Stop();
                  return;
               }
               bag.Add(i);
            });
            stopWatch.Stop();
            Console.WriteLine("Bag count is " + bag.Count + ", " + stopWatch.ElapsedMilliseconds);
         }
複製程式碼

這裡使用的是Stop,當數量達到300個時,會立刻停止;可以看到結果"Bag count is 300",如果用break,可能結果是300多個或者300個,大家可以測試一下。

2、異常處理

  首先任務是平行計算的,處理過程中可能會產生n多的異常,那麼如何來獲取到這些異常呢?普通的Exception並不能獲取到異常,然而為並行誕生的AggregateExcepation就可以獲取到一組異常。

這裡我們修改Parallel.Invoke的程式碼,修改後程式碼如下:

複製程式碼
public class ParallelDemo
      {
         private Stopwatch stopWatch = new Stopwatch();
     </span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> Run1()
     {
        Thread.Sleep(</span><span style="color:#800080;">2000</span><span style="color:#000000;">);
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Task 1 is cost 2 sec</span><span style="color:#800000;">"</span><span style="color:#000000;">);
        </span><span style="color:#0000ff;">throw</span> <span style="color:#0000ff;">new</span> Exception(<span style="color:#800000;">"</span><span style="color:#800000;">Exception in task 1</span><span style="color:#800000;">"</span><span style="color:#000000;">);
     }
     </span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> Run2()
     {
        Thread.Sleep(</span><span style="color:#800080;">3000</span><span style="color:#000000;">);
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Task 2 is cost 3 sec</span><span style="color:#800000;">"</span><span style="color:#000000;">);
        </span><span style="color:#0000ff;">throw</span> <span style="color:#0000ff;">new</span> Exception(<span style="color:#800000;">"</span><span style="color:#800000;">Exception in task 2</span><span style="color:#800000;">"</span><span style="color:#000000;">);
     }

     </span><span style="color:#0000ff;">public</span> <span style="color:#0000ff;">void</span><span style="color:#000000;"> ParallelInvokeMethod()
     {
        stopWatch.Start();
        </span><span style="color:#0000ff;">try</span><span style="color:#000000;">
        {
           Parallel.Invoke(Run1, Run2);
        }
        </span><span style="color:#0000ff;">catch</span><span style="color:#000000;"> (AggregateException aex)
        {
           </span><span style="color:#0000ff;">foreach</span> (<span style="color:#0000ff;">var</span> ex <span style="color:#0000ff;">in</span><span style="color:#000000;"> aex.InnerExceptions)
           {
              Console.WriteLine(ex.Message);
           }
        }</span><span style="color:#000000;">
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Parallel run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);

        stopWatch.Reset();
        stopWatch.Start();
        </span><span style="color:#0000ff;">try</span><span style="color:#000000;">
        {
           Run1();
           Run2();
        }
        </span><span style="color:#0000ff;">catch</span><span style="color:#000000;">(Exception ex)
        {
           Console.WriteLine(ex.Message);
        }</span><span style="color:#000000;">
        stopWatch.Stop();
        Console.WriteLine(</span><span style="color:#800000;">"</span><span style="color:#800000;">Normal run </span><span style="color:#800000;">"</span> + stopWatch.ElapsedMilliseconds + <span style="color:#800000;">"</span><span style="color:#800000;"> ms.</span><span style="color:#800000;">"</span><span style="color:#000000;">);
     }

複製程式碼

順序呼叫方法我把異常處理寫一起了,這樣只能捕獲Run1的異常資訊,大家可以分開寫。捕獲AggregateException 異常後,用foreach迴圈遍歷輸出異常資訊,可以看到兩個異常資訊都顯示了。