C# 4.0 並行計算部分 [轉發]

沿用微軟的寫法,System.Threading.Tasks.::.Parallel類,提供對並行循環和區域的支持。 我們會用到的方法有For,ForEach,Invoke。

一、簡單使用

        首先我們初始化一個List用於循環,這裏我們循環10次。(後面的代碼都會按這個標準進行循環)

Code
  1.             Program .Data = new List <int >();
  2.             for (int i = 0; i < 10; i++)
  3.             {
  4.                 Data.Add(i);
  5.             }

        下面我們定義4個方法,分別爲for,foreach,並行For,並行ForEach。並測試他們的運行時長。

Code
  1.         /// <summary>
  2.         /// 是否顯示執行過程
  3.         /// </summary>
  4.         public bool ShowProcessExecution = false ;
  5.         /// <summary>
  6.         /// 這是普通循環for
  7.         /// </summary>
  8.         private void Demo1()
  9.         {
  10.             List <int > data = Program .Data;
  11.             DateTime dt1 = DateTime .Now;
  12.             for (int i = 0; i < data.Count; i++)
  13.             {
  14.                 Thread .Sleep(500);
  15.                 if (ShowProcessExecution)
  16.                     Console .WriteLine(data[i]);
  17.             }
  18.             DateTime dt2 = DateTime .Now;
  19.             Console .WriteLine("普通循環For運行時長:{0}毫秒。" , (dt2 - dt1).TotalMilliseconds);
  20.         }
  21.         /// <summary>
  22.         /// 這是普通循環foreach
  23.         /// </summary>
  24.         private void Demo2()
  25.         {
  26.             List <int > data = Program .Data;
  27.             DateTime dt1 = DateTime .Now;
  28.             foreach (var in data)
  29.             {
  30.                 Thread .Sleep(500);
  31.                 if (ShowProcessExecution)
  32.                     Console .WriteLine(i);
  33.             }
  34.             DateTime dt2 = DateTime .Now;
  35.             Console .WriteLine("普通循環For運行時長:{0}毫秒。" , (dt2 - dt1).TotalMilliseconds);
  36.         }
  37.         /// <summary>
  38.         /// 這是並行計算For
  39.         /// </summary>
  40.         private void Demo3()
  41.         {
  42.             List <int > data = Program .Data;
  43.             DateTime dt1 = DateTime .Now;
  44.             Parallel .For(0, data.Count, (i) =>
  45.             {
  46.                 Thread .Sleep(500);
  47.                 if (ShowProcessExecution)
  48.                     Console .WriteLine(data[i]);
  49.             });
  50.             DateTime dt2 = DateTime .Now;
  51.             Console .WriteLine("並行運算For運行時長:{0}毫秒。" , (dt2 - dt1).TotalMilliseconds);
  52.         }
  53.         /// <summary>
  54.         /// 這是並行計算ForEach
  55.         /// </summary>
  56.         private void Demo4()
  57.         {
  58.             List <int > data = Program .Data;
  59.             DateTime dt1 = DateTime .Now;
  60.             Parallel .ForEach(data, (i) =>
  61.             {
  62.                 Thread .Sleep(500);
  63.                 if (ShowProcessExecution)
  64.                     Console .WriteLine(i);
  65.             });
  66.             DateTime dt2 = DateTime .Now;
  67.             Console .WriteLine("並行運算ForEach運行時長:{0}毫秒。" , (dt2 - dt1).TotalMilliseconds);
  68.         }

下面是運行結果:

image

這裏我們可以看出並行循環在執行效率上的優勢了。

結論1:在對一個數組內的每一個項做單獨處理時,完全可以選擇並行循環的方式來提升執行效率。

原理1:並行計算的線程開啓是緩步開啓的,線程數量1,2,4,8緩步提升。(不詳,PLinq最多64個線程,可能這也是64)

   

   

二、 並行循環的中斷和跳出

        當在進行循環時,偶爾會需要中斷循環或跳出循環。下面是兩種跳出循環的方法Stop和Break,LoopState是循環狀態的參數。

Code
  1.         /// <summary>
  2.         /// 中斷Stop
  3.         /// </summary>
  4.         private void Demo5()
  5.         {
  6.             List <int > data = Program .Data;
  7.             Parallel .For(0, data.Count, (i, LoopState) =>
  8.             {
  9.                 if (data[i] > 5)
  10.                     LoopState.Stop();
  11.                 Thread .Sleep(500);
  12.                 Console .WriteLine(data[i]);
  13.             });
  14.             Console .WriteLine("Stop執行結束。" );
  15.         }
  16.         /// <summary>
  17.         /// 中斷Break
  18.         /// </summary>
  19.         private void Demo6()
  20.         {
  21.             List <int > data = Program .Data;
  22.             Parallel .ForEach(data, (i, LoopState) =>
  23.             {
  24.                 if (i > 5)
  25.                     LoopState.Break();
  26.                 Thread .Sleep(500);
  27.                 Console .WriteLine(i);
  28.             });
  29.             Console .WriteLine("Break執行結束。" );
  30.         }

        執行結果如下:

image

結論2:使用Stop會立即停止循環,使用Break會執行完畢所有符合條件的項。

   

   

三、並行循環中爲數組/集合添加項

        上面的應用場景其實並不是非常多見,畢竟只是爲了遍歷一個數組內的資源,我們更多的時候是爲了遍歷資源,找到我們所需要的。那麼請繼續看。

下面是我們一般會想到的寫法:

Code
  1.         private void Demo7()
  2.         {
  3.             List <int > data = new List <int >();
  4.             Parallel .For(0, Program .Data.Count, (i) =>
  5.             {
  6.                 if (Program .Data[i] % 2 == 0)
  7.                     data.Add(Program .Data[i]);
  8.             });
  9.             Console .WriteLine("執行完成For." );
  10.         }
  11.         private void Demo8()
  12.         {
  13.             List <int > data = new List <int >();
  14.             Parallel .ForEach(Program .Data, (i) =>
  15.             {
  16.                 if (Program .Data[i] % 2 == 0)
  17.                     data.Add(Program .Data[i]);
  18.             });
  19.             Console .WriteLine("執行完成ForEach." );
  20.         }

看起來應該是沒有問題的,但是我們多次運行後會發現,偶爾會出現錯誤如下:

image

這是因爲List是非線程安全的類,我們需要使用System.Collections.Concurrent命名空間下的類型來用於並行循環體內。

 

說明
BlockingCollection<T> 爲實現 IProducerConsumerCollection<T> 的線程安全集合提供阻止和限制功能。
ConcurrentBag<T> 表示對象的線程安全的無序集合。
ConcurrentDictionary<TKey, TValue> 表示可由多個線程同時訪問的鍵值對的線程安全集合。
ConcurrentQueue<T> 表示線程安全的先進先出 (FIFO) 集合。
ConcurrentStack<T> 表示線程安全的後進先出 (LIFO) 集合。
OrderablePartitioner<TSource> 表示將一個可排序數據源拆分成多個分區的特定方式。
Partitioner 提供針對數組、列表和可枚舉項的常見分區策略。
Partitioner<TSource> 表示將一個數據源拆分成多個分區的特定方式。

公共類

那麼我們上面的代碼可以修改爲,加了了ConcurrentQueue和ConcurrentStack的最基本的操作。

Code
  1.         /// <summary>
  2.         /// 並行循環操作集合類,集合內只取5個對象
  3.         /// </summary>
  4.         private void Demo7()
  5.         {
  6.             ConcurrentQueue <int > data = new ConcurrentQueue <int >();
  7.             Parallel .For(0, Program .Data.Count, (i) =>
  8.             {
  9.                 if (Program .Data[i] % 2 == 0)
  10.                     data.Enqueue(Program .Data[i]);//將對象加入到隊列末尾
  11.             });
  12.             int R;
  13.             while (data.TryDequeue(out R))//返回隊列中開始處的對象
  14.             {
  15.                 Console .WriteLine(R);
  16.             }
  17.             Console .WriteLine("執行完成For." );
  18.         }
  19.         /// <summary>
  20.         /// 並行循環操作集合類
  21.         /// </summary>
  22.         private void Demo8()
  23.         {
  24.             ConcurrentStack <int > data = new ConcurrentStack <int >();
  25.             Parallel .ForEach(Program .Data, (i) =>
  26.             {
  27.                 if (Program .Data[i] % 2 == 0)
  28.                     data.Push(Program .Data[i]);//將對象壓入棧中
  29.             });
  30.             int R;
  31.             while (data.TryPop(out R))//彈出棧頂對象
  32.             {
  33.                 Console .WriteLine(R);
  34.             }
  35.             Console .WriteLine("執行完成ForEach." );
  36.         }

ok,這裏返回一個序列的問題也解決了。

結論3:在並行循環內重複操作的對象,必須要是thread-safe(線程安全)的。集合類的線程安全對象全部在System.Collections.Concurrent命名空間下。

   

   

四、返回集合運算結果/含有局部變量的並行循環

        使用循環的時候經常也會用到迭代,那麼在並行循環中叫做 含有局部變量的循環 。下面的代碼中詳細的解釋,這裏就不囉嗦了。

Code
  1.         /// <summary>
  2.         /// 具有線程局部變量的For循環
  3.         /// </summary>
  4.         private void Demo9()
  5.         {
  6.             List <int > data = Program .Data;
  7.             long total = 0;
  8.             //這裏定義返回值爲long類型方便下面各個參數的解釋
  9.             Parallel .For<long >(0,           // For循環的起點
  10.                 data.Count,                 // For循環的終點
  11.                 () => 0,                    // 初始化局部變量的方法(long),既爲下面的subtotal的初值
  12.                 (i, LoopState, subtotal) => // 爲每個迭代調用一次的委託,i是當前索引,LoopState是循環狀態,subtotal爲局部變量名
  13.                 {
  14.                     subtotal += data[i];    // 修改局部變量
  15.                     return subtotal;        // 傳遞參數給下一個迭代
  16.                 },
  17.                 (finalResult) => Interlocked .Add(ref total, finalResult) //對每個線程結果執行的最後操作,這裏是將所有的結果相加
  18.                 );
  19.             Console .WriteLine(total);
  20.         }
  21.         /// <summary>
  22.         /// 具有線程局部變量的ForEach循環
  23.         /// </summary>
  24.         private void Demo10()
  25.         {
  26.             List <int > data = Program .Data;
  27.             long total = 0;
  28.             Parallel .ForEach<int , long >(data, // 要循環的集合對象
  29.                 () => 0,                      // 初始化局部變量的方法(long),既爲下面的subtotal的初值
  30.                 (i, LoopState, subtotal) =>   // 爲每個迭代調用一次的委託,i是當前元素,LoopState是循環狀態,subtotal爲局部變量名
  31.                 {
  32.                     subtotal += i;            // 修改局部變量
  33.                     return subtotal;          // 傳遞參數給下一個迭代
  34.                 },
  35.                 (finalResult) => Interlocked .Add(ref total, finalResult) //對每個線程結果執行的最後操作,這裏是將所有的結果相加
  36.                 );
  37.             Console .WriteLine(total);
  38.         }

結論4:並行循環中的迭代,確實很傷人。代碼太難理解了。

五、PLinq(Linq的並行計算)

           上面介紹完了For和ForEach的並行計算盛宴,微軟也沒忘記在Linq中加入並行計算。下面介紹Linq中的並行計算。

4.0中在System.Linq命名空間下加入了下面幾個新的類:

 

說明
ParallelEnumerable 提供一組用於查詢實現 ParallelQuery{TSource} 的對象的方法。這是 Enumerable 的並行等效項。
ParallelQuery 表示並行序列。
ParallelQuery<TSource> 表示並行序列。

原理2:PLinq最多會開啓64個線程

原理3:PLinq會自己判斷是否可以進行並行計算,如果不行則會以順序模式運行。

原理4:PLinq會在昂貴的並行算法或成本較低的順序算法之間進行選擇,默認情況下它選擇順序算法。

   

在ParallelEnumerable中提供的並行化的方法

 

ParallelEnumerable 運算符 說明
AsParallel() PLINQ 的入口點。指定如果可能,應並行化查詢的其餘部分。
AsSequential() 指定查詢的其餘部分應像非並行 LINQ 查詢一樣按順序運行。
AsOrdered() 指定 PLINQ 應保留查詢的其餘部分的源序列排序,直到例如通過使用 orderby 子句更改排序爲止。
AsUnordered() 指定查詢的其餘部分的 PLINQ 不需要保留源序列的排序。
WithCancellation() 指定 PLINQ 應定期監視請求取消時提供的取消標記和取消執行的狀態。
WithDegreeOfParallelism() 指定 PLINQ 應當用來並行化查詢的處理器的最大數目。
WithMergeOptions() 提供有關 PLINQ 應當如何(如果可能)將並行結果合併回到使用線程上的一個序列的提示。
WithExecutionMode() 指定 PLINQ 應當如何並行化查詢(即使默認行爲是按順序運行查詢)。
ForAll() 多線程枚舉方法,與循環訪問查詢結果不同,它允許在不首先合併回到使用者線程的情況下並行處理結果。
Aggregate() 重載 對於 PLINQ 唯一的重載,它啓用對線程本地分區的中間聚合以及一個用於合併所有分區結果的最終聚合函數。

下面是PLinq的簡單代碼

Code
  1.         /// <summary>
  2.         /// PLinq簡介
  3.         /// </summary>
  4.         private void Demo11()
  5.         {
  6.             var source = Enumerable .Range(1, 10000);
  7.             //查詢結果按source中的順序排序
  8.             var evenNums = from num in source.AsParallel().AsOrdered()
  9.                        where num % 2 == 0
  10.                        select num;
  11.             //ForAll的使用
  12.             ConcurrentBag <int > concurrentBag = new ConcurrentBag <int >();
  13.             var query = from num in source.AsParallel()
  14.                         where num % 10 == 0
  15.                         select num;
  16.             query.ForAll((e) => concurrentBag.Add(e * e));
  17.         }

上面代碼中使用了ForAll,ForAll和foreach的區別如下:

image

 

PLinq的東西很繁雜,但是都只是幾個簡單的方法,熟悉下方法就好了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章