C#並行程式設計:Parallel的使用
前言:在C#的System.Threading.Tasks 名稱空間中有一個靜態的並行類:Parallel,封裝了Task的使用,對於執行大量任務提供了非常簡便的操作。下面對他的使用進行介紹。
本篇內容:
1.1、Parallel.For 使用
1.2、Parallel.ForEach 使用
1.3、Parallel.Invoke 使用
1.4、ParallelOptions 選項配置
1.5、ParallelLoopResult 執行結果
1.6、ParallelLoopState 提前結束
1.7、Parallel的使用場景分析
1.1、Parallel.For 使用
首先建立一個控制檯程式,本案例使用的是.net core 3.1,引入名稱空間using System.Threading。假設某個操作需要執行10次,從0到9程式碼如下:
static void ParallelFor(int num) { Console.WriteLine($"ParallelFor執行 {num} 次"); var list = new List(num); ParallelLoopResult result = Parallel.For(0, num, i => { list.Add(new Product { Id = i, Name = "TestName" }); Console.WriteLine($View Code"Task Id:{Task.CurrentId},Thread: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(10); }); }
執行結果如下:
從列印資訊可以看出,任務Id和執行緒都是無序的,在使用時需要注意。
Parallel.For 還提供了很多過載本版:
我們看一下帶ParallelLoopState 引數的一個過載版本:ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int, ParallelLoopState> body)
測試程式碼:
/// <summary> /// 提前終止 /// </summary> static void ParallelForAsyncAbort() { ParallelLoopResult result = Parallel.For(10, 100, async (int index, ParallelLoopState pls) => { Console.WriteLine($"index:{index} task:{Task.CurrentId},Thread:{Thread.CurrentThread.ManagedThreadId}"); await Task.Delay(10); if (index > 30) pls.Break(); }); Console.WriteLine($"Is completed:{result.IsCompleted} LowestBreakIteration:{result.LowestBreakIteration}"); }View Code
執行結果:
任務提前結束了,最小執行Break方法的索引為19
帶引數:ParallelOptions的過載版本:
/// <summary> /// 執行500毫秒後取消 /// </summary> static void ParalletForCancel() { var cts = new CancellationTokenSource(); cts.Token.Register(() => { Console.WriteLine($"*** token canceled"); } ); // send a cancel after 500ms cts.CancelAfter(500); try { ParallelLoopResult result = Parallel.For(0, 100, new ParallelOptions() { CancellationToken = cts.Token, }, x => { Console.WriteLine($"loop {x} started"); int sum = 0; for (int i = 0; i < 100; i++) { Thread.Sleep(2); sum += i; } Console.WriteLine($"loop {x} finished"); }); } catch (OperationCanceledException ex) { Console.WriteLine(ex.Message); } }View Code
任務執行一段時間後取消了
1.2、Parallel.ForEach 使用
ForEach方法可用於對集合,陣列,或列舉進行迴圈操作,下面進行簡單使用:
//簡單使用 static void ParallelForEach() { string[] data = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" }; ParallelLoopResult result = Parallel.ForEach(data, a => { Console.WriteLine(a); }); }View Code
執行結果:
請注意迴圈的執行是無序的,我們打印出執行順序:
//帶索引的迴圈操作 static void ParallelForEach2() { string[] data = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" }; ParallelLoopResult result = Parallel.ForEach<string>(data, (str, psl, index) => { Console.WriteLine($"str:{str} index:{index}"); }); }View Code
執行結果:
ForEach同樣支援提前結束和取消操作:
1.3、Parallel.Invoke 使用
Invoke主要用於操作(任務)並行,能同時執行多個操作,並儘可能的同時執行。
簡單使用:
static void ParallelInvoke() { Parallel.Invoke(Foo, Bar); } static void Foo() { Console.WriteLine("Foo"); } static void Bar() { Console.WriteLine("Bar"); }View Code
大於10個操作:
static void ParallelInvoke2() { Action action = () => { Console.WriteLine($"Thread Id:{Thread.CurrentThread.ManagedThreadId}"); }; Parallel.Invoke(action, action, action, action, action, action, action, action, action, action, action); Console.WriteLine("Parallel.Invoke 執行完畢"); }View Code
如果任務不超過10個,Invoke內部會使用Task.Factory.StartNew 建立任務,效率不高,不如直接使用Task。
1.4、ParallelOptions 選項配置
ParallelOptions是一個選項配置,有三個屬性:
1.4.1、CancellationToken-定義取消令牌,處理任務被取消後的一些操作
1.4.2、MaxDegreeOfParallelism-設定最大併發限制,預設-1
1.4.3、TaskScheduler 指定任務排程器
1.5、ParallelLoopResult 執行結果
ParallelLoopResult,併發迴圈結果,有兩個屬性:
IsCompleted-任務是否執行完
LowestBreakIteration-呼叫Break方法的最小任務的索引
1.6、ParallelLoopState 提前結束
ParallelLoopState 用於提前結束迴圈操作,比如搜尋演算法,已找到結果提前結束查詢。
有兩個方法:
Break:告知Parallel迴圈應在系統方便的時候儘早停止執行當前迭代之外的迭代
Stop:告知Parallel迴圈應在系統方便的時候儘早停止執行。
如果迴圈之外還有需要執行的程式碼則用Break,否則使用Stop
1.7、Parallel的使用場景分析
1.7.1、Parallel.Invoke 使用特點:
1、如果操作小於10個,使用Task.Factory.StartNew 或者Task.Run 效率更高
2、適合用於執行大量操作且無需返回結果的場景
1.7.2、Parallel.For 使用特點:
1、帶索引的大量迴圈操作
1.7.3、Parallel.ForEach 使用特點:
1、大資料集(陣列,集合,列舉集)的迴圈執行
1.7.4、注意事項:
1、迴圈操作是無序的,如果需要順序直接請使用同步執行
2、如果涉及操作共享變數請使用執行緒同步鎖
3、如果是簡單、量大且無等待的操作可能並不適用,同步執行可能更快
4、注意錯誤的處理,如果是帶資料庫的操作請注意事務的使用
5、個人測試,Parallel.ForEach 的使用效率比Parallel.For更高
效能測試程式碼如下:
#region 效能測試 private static void TestPerformance() { int num = 10; Console.WriteLine($"測試執行:{num}次"); Console.WriteLine(); Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); ParallelFor(num); stopwatch.Stop(); Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}"); Console.WriteLine(); stopwatch.Restart(); ParallelForEach(num); stopwatch.Stop(); Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}"); Console.WriteLine(); stopwatch.Restart(); Sync(num); stopwatch.Stop(); Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}"); Console.WriteLine(); stopwatch.Restart(); TaskTest(num); stopwatch.Stop(); Console.WriteLine($"耗時:{stopwatch.ElapsedMilliseconds}"); } static void ParallelFor(int num) { Console.WriteLine($"ParallelFor執行 {num} 次"); var list = new List<Product>(num); ParallelLoopResult result = Parallel.For(0, num, i => { list.Add(new Product { Id = i, Name = "TestName" }); //去掉Thread的程式碼模擬簡單業務操作 Thread.Sleep(10); }); } static void Sync(int num) { Console.WriteLine($"同步執行 {num} 次"); var list = new List<Product>(num); for (int i = 0; i < num; i++) { list.Add(new Product { Id = i, Name = "TestName" }); Thread.Sleep(10); } } static void ParallelForEach(int num) { string[] datas = new string[num]; Console.WriteLine($"ParallelForEach執行 {num} 次"); var list = new List<Product>(num); Parallel.ForEach(datas, (s, pls, i) => { list.Add(new Product { Id = (int)i, Name = "TestName" }); Thread.Sleep(10); }); } static void TaskTest(int num) { Console.WriteLine($"Task 執行 {num} 次"); var list = new List<Product>(num); while (num > 0) { Task.Run(() => { list.Add(new Product { Id = num, Name = "TestName" }); }); Thread.Sleep(10); num--; } } #endregionView Code
簡單任務效能測試:
複雜任務效能測試模擬:
以上是我對Parallel的學習和使用經驗總結,歡迎大家一起交流和學習。
參考:《C#高階程式設計第4版》、微軟官網
本人專注於.net平臺開發,擅長開發企業管理系統,CRM系統,ERP系統,財務系統,許可權系統,非常樂意跟大家討論相關係統的設計和開發技巧