閱讀目錄html
任務並行庫(Task Parallel Library)是BCL中的一個類庫,極大地簡化了並行編程,Parallel經常使用的方法有For/ForEach/Invoke三個靜態方法。在C#中for/foreach循環使用十分廣泛,若是迭代不依賴與上次迭代的結果時,把迭代放在 不一樣的處理器上並行處理 將很大地提升運行效率,Parallel.For和Parallel.ForEach就是爲這個目的而設計的。編程
看一個Parallel.For/ForEach的栗子:windows
static void Main(string[] args) { //Parallel.For 計算0到6的平方 Parallel.For(1, 6, i => { Console.WriteLine($"{i}的平方是{i*i}"); }); //Parallel.ForEach 計算每一個字符串的長度 string[] strs = { "We", "hold", "these", "truths" }; Parallel.ForEach(strs, i => Console.WriteLine($"{i}有{i.Length}個字節")); Console.ReadKey(); }
運行結果:網絡
若是咱們想並行執行多個任務,可使用 Parallel.Invoke(Action[] actions) 方法,看一個栗子:多線程
static void Main(string[] args) { Parallel.Invoke( () => { Console.WriteLine($"並行執行任務1,線程Id爲{Thread.CurrentThread.ManagedThreadId}"); }, () => { Console.WriteLine($"並行執行任務2,線程Id爲{Thread.CurrentThread.ManagedThreadId}"); } ); Console.ReadKey(); }
執行結果以下:異步
計時器提供了一種 按期重複運行異步方法 的方式,當計時器到期後,系統從線程池中的線程上開啓一個回調方法,把state做爲參數,並開始運行。異步編程
Timer最經常使用的構造函數以下:函數
Timer(TimeCallback callback,object state,uint dueTime, uint period)
callback是一個返回值爲void的委託,state爲傳入callback的參數,dueTime爲第一次調用前的時間,period爲兩次調用的時間間隔優化
一個栗子:ui
1 class Program 2 { 3 int count = 0; 4 void Run(object state) 5 { 6 Console.WriteLine("{0},已經調用了{1}次了", state, ++count); 7 } 8 static void Main(string[] args) 9 { 10 Program p = new Program(); 11 //2000毫秒後開始調用,每次間隔1000毫秒 12 Timer timer = new Timer(p.Run, "hello", 2000, 1000); 13 Console.WriteLine("Timer start"); 14 15 Console.ReadLine(); 16 } 17 }
執行結果:
委託執行異步是早期執行異步的一種方式,特別是早幾年進行網絡編程時用的比較多。如今咱們徹底可使用更優秀的其餘異步編程模式去替代它。有時候咱們會查看早期的代碼,咱們在這裏簡單介紹下委託執行異步的方法。使用委託執行異步,使用的是引用方法,若是一個委託對象在調用列表中只有一個方法(這個方法就是引用方法),它就能夠異步執行這個方法。委託類有兩個方法 BeginIvoke和EndInvoke 。
BeginInvoke :執行BeginInvoke方法時,會線程池中獲取一個獨立線程來執行引用方法,並當即返回一個實現IAsyncResult接口的對象的(該對象包含了線程池中線程運行異步方法的狀態),調用線程不阻塞,而引用方法在線程池的線程中並行執行。
EndInvoke : 獲取異步方法調用返回的值,並釋放資源,該方法把異步方法的返回值做爲本身的返回值。
委託執行異步編程的3種模式:
等待一直到完成(wait-until-done):在發起了異步方法,原始線程執行到EndInvoke時就中斷而且等異步方法完成完成後再繼續。
輪詢(polling):原始線程按期檢查發起的線程是否完成(經過IAsyncResult.IsCompleted屬性判斷),若是沒有則繼續進行原始線程中的任務。
回調(callback):原始線程一直執行,無需等待或檢查發起的線程是否完成,在發起的線程中的引用方法完成以後,發起線程會調用回調方法,由回調方法在調用EndInvoke以前處理異步方法的結果。
原始線程執行到EndInvoke,若是異步任務沒有完成就一直等待
1 delegate int MyDel(int first,int second);//委託聲明 2 class Program 3 { 4 static int Sum(int x, int y) 5 { 6 Thread.Sleep(1000); 7 return x + y; 8 } 9 static void Main(string[] args) 10 { 11 MyDel del = Sum; 12 //調用異步操做(第三個參數是回調函數,第四個參數是額外的值) 13 IAsyncResult iar = del.BeginInvoke(3, 5, null, null); 14 15 //doSomehing... 16 17 //執行EndInvoke,若是引用方法Sum沒有執行完成,主線程就等待其完成 18 int result = del.EndInvoke(iar); 19 Console.WriteLine(result); 20 } 21 }
按期查詢任務是否完成:
1 delegate int MyDel(int first,int second);//委託聲明 2 class Program 3 { 4 static int Sum(int x, int y) 5 { 6 Thread.Sleep(1000); 7 return x + y; 8 } 9 static void Main(string[] args) 10 { 11 MyDel del = Sum; 12 IAsyncResult iar = del.BeginInvoke(3, 5, null, null); 13 14 //經過iar.IsCompleted按期查詢完成狀態 15 while (!iar.IsCompleted)//IsCompleted表示調用的異步操做是否完成 16 { 17 //doSomething 18 Thread.Sleep(300); 19 Console.WriteLine("no done"); 20 } 21 int result = del.EndInvoke(iar); 22 Console.WriteLine(result); 23 Console.ReadKey(); 24 } 25 }
原始線程執行委託的BeginInvoke後就無論新線程的事了,委託中的引用方法執行完成後,在回調函數中獲取結果並處理,執行委託的EndInvoke方法
1 delegate int MyDel(int first,int second);//委託聲明 2 class Program 3 { 4 static int Sum(int x, int y) 5 { 6 Thread.Sleep(1000); 7 return x + y; 8 } 9 10 //回調方法的簽名和返回值類型必須和AsyncCallBack委託類型一致 11 //輸入參數爲IAsyncResult,返回值是Void類型 12 static void CallWhenDone(IAsyncResult iar){ 13 AsyncResult ar = (AsyncResult)iar; 14 MyDel del = (MyDel)ar.AsyncDelegate; 15 int result = del.EndInvoke(iar); 16 Console.WriteLine("回調函數執行EndInvoke"); 17 Console.WriteLine("result:{0}", result); 18 Console.WriteLine("回調函數完成"); 19 } 20 21 static void Main(string[] args) 22 { 23 MyDel del = Sum; 24 //執行BeginInvoke方法後原始線程就不用管了,在自定義的回調函數(CallWhenDone)中執行EndInvoke方法 25 IAsyncResult iar = del.BeginInvoke(3, 5, CallWhenDone, null); 26 Console.WriteLine("開啓新線程,異步任務完成後執行回調函數"); 27 //doSomething 28 Console.WriteLine("回調執行不阻塞原始線程"); 29 Console.ReadKey(); 30 } 31 }
執行結果:
還有一些其餘的異步編程模式如BackgroundWorker等,這裏再也不過多介紹。
咱們使用多線程時有時會遇到cpu佔用太高、內存爆滿的狀況,快速定位異常線程是多線程開發中必須熟悉的技能。cpu佔用太高通常是由死循環形成的,看下邊一個簡單的栗子,Run方法內部有死循環,程序運行後會 佔用大量的cpu資源:
namespace MyApp { class Program { static void Main(string[] args) { Run(); Run2(); Console.ReadKey(); } //死循環,會形成cpu內存佔用太高 static void Run() { Thread th = new Thread(() => { while (true) { Console.WriteLine("hello windbg"); } }); th.Start(); } //不會佔用過高的cpu資源 static void Run2() { Thread th = new Thread(() => { while (true) { Thread.Sleep(1000); Console.WriteLine("hello windbg2"); } }); th.Start(); } } }
程序運行後cpu資源佔用太高,怎麼去定位呢?這裏採用Windbg簡單演示cpu佔用太高的異常定位,下載地址:Windbg下載。安裝完成後,界面以下所示:
1.生成Dump文件
這裏MyApp生成爲x64位的Release版本,點擊MyApp.exe文件運行,打開【任務管理器】,找到MyApp,右鍵選擇【建立轉儲文件】便可生成dump文件。
2.Windbg分析dump文件
打開Windbg,選擇【文件】->【Open dump file】->找到上一步生成的dump文件便可。
執行如下命令加載符號和sos庫
.sympath SRV*c:\localsymbols*http://msdl.microsoft.com/download/symbols .reload
.load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.DLL
經過命令 !threads 查看線程:
死循環會長期佔有cpu,經過 !runaway 查看各個線程的運行時間:
咱們看到 4eac線程的運行時間最長,經過命令 ~~[4eac] ; !clrstack 查看線程堆棧信息:
咱們看到異常定位在MyApp的Program類的第24行,查看咱們的代碼,找到這個位置,發現這裏是一個while(true)死循環,定位結束。
內存爆滿也是異常遇到的問題,如大量拼接字符串會佔用較大的內存,看下邊的一個栗子,程序代碼以下:
class Program { static void Main(string[] args) { Console.WriteLine("開始執行.."); GetBigString(); Console.ReadKey(); } //大字符串拼接 static void GetBigString() { String str = ""; for (int i = 0; i < 10000000; i++) { str+=$"hello{i}"; } Console.WriteLine(str); } }
內存爆滿最多見緣由是大量建立某個類型的變量,問題定位方法和上邊定位cpu佔用高的定位差很少。首先生成dump文件,而後用Windbg打開,加載符號和sos庫,而後執行 !dumpheap –stat 查看各個類型的數量和尺寸,咱們看到string類型數量和佔用的資源不少:
經過 !DumpHeap /d -mt 00007ff8878c74c0 查看當前的方法表,以下:
點開一個地址,具體內容以下:
經過字符串內容是【hello0hello1...】和string類型數量多、尺寸大,咱們再去在代碼中查找很容易定位到問題代碼。
小結:Windbg能夠查看clr級別內容,在開發中對咱們優化代碼和異常定位有不錯的幫助,這裏只是簡單介紹Windbg的基本用法,有興趣的小夥伴能夠研究下官方教程。