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();

 public void Run1()
 {
 Thread.Sleep(2000);
 Console.WriteLine("Task 1 is cost 2 sec");
 }
 public void Run2()
 {
 Thread.Sleep(3000);
 Console.WriteLine("Task 2 is cost 3 sec");
 }

 public void ParallelInvokeMethod()
 {
 stopWatch.Start();
 Parallel.Invoke(Run1, Run2);
 stopWatch.Stop();
 Console.WriteLine("Parallel run " + stopWatch.ElapsedMilliseconds + " ms.");
 
 stopWatch.Restart();
 Run1();
 Run2();
 stopWatch.Stop();
 Console.WriteLine("Normal run " + stopWatch.ElapsedMilliseconds + " ms.");
 }
}

代碼很簡單,首先新加一個類,在類中寫瞭兩個方法,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(0, 10000, item =>
 {
 for (int j = 0; j < 60000; j++)
 {
  int sum = 0;
  sum += item;
 }
 });
 stopWatch.Stop();
 Console.WriteLine("ParallelFor run " + stopWatch.ElapsedMilliseconds + " ms.");
 
 }

寫瞭兩個循環,做瞭一些沒有意義的事情,目的主要是為瞭消耗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();
 for (int i = 0; i < 10000; i++)
 {
 for (int j = 0; j < 60000; j++)
 {
  //int sum = 0;
  //sum += item;
  num++;
 }
 }
 stopWatch.Stop();
 Console.WriteLine("NormalFor run " + stopWatch.ElapsedMilliseconds + " ms.");

 stopWatch.Reset();
 stopWatch.Start();
 Parallel.For(0, 10000, item =>
 {
 for (int j = 0; j < 60000; j++)
 {
  //int sum = 0;
  //sum += item;
  lock (obj)
  {
  num++;
  }
 }
 });
 stopWatch.Stop();
 Console.WriteLine("ParallelFor run " + stopWatch.ElapsedMilliseconds + " ms.");
 
 }

Parallel.For由於是並行運行的,所以會同時訪問全局變量num,為瞭得到正確的結果,要使用lock,此時來看看運行結果:

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

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

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

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

3、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();

 public void Run1()
 {
 Thread.Sleep(2000);
 Console.WriteLine("Task 1 is cost 2 sec");
 throw new Exception("Exception in task 1");
 }
 public void Run2()
 {
 Thread.Sleep(3000);
 Console.WriteLine("Task 2 is cost 3 sec");
 throw new Exception("Exception in task 2");
 }

 public void ParallelInvokeMethod()
 {
 stopWatch.Start();
 try
 {
 Parallel.Invoke(Run1, Run2);
 }
 catch (AggregateException aex)
 {
 foreach (var ex in aex.InnerExceptions)
 {
  Console.WriteLine(ex.Message);
 }
 }
 stopWatch.Stop();
 Console.WriteLine("Parallel run " + stopWatch.ElapsedMilliseconds + " ms.");

 stopWatch.Reset();
 stopWatch.Start();
 try
 {
 Run1();
 Run2();
 }
 catch(Exception ex)
 {
 Console.WriteLine(ex.Message);
 }
 stopWatch.Stop();
 Console.WriteLine("Normal run " + stopWatch.ElapsedMilliseconds + " ms.");
 }
}

順序調用方法我把異常處理寫一起瞭,這樣隻能捕獲Run1的異常信息,大傢可以分開寫。捕獲AggregateException 異常後,用foreach循環遍歷輸出異常信息,可以看到兩個異常信息都顯示瞭。

點擊這裡,下載源碼

以上就是c# 並行和多線程編程——認識Parallel的詳細內容,更多關於c# 並行和多線程編程的資料請關註WalkonNet其它相關文章!