你們知道「並行」是利用CPU的多個核心或者多個CPU同時執行不一樣的任務,咱們不關心這些任務之間的依賴關係。
可是在咱們實際的業務中,不少任務之間是相互影響的,好比統計車間整年產量的運算要依賴於各月產量的統計結果。假如你想在計算月產量的時候作些其餘事情,如導出生產異常報表,「異步」就能夠登上舞臺了。html
說到異步,必需要先提一下同步。一圖勝千言:編程
圖中操做C的執行依賴B的結果,B的執行依賴A的結果。線程1連續執行操做A、B、C即是一個同步過程;相對地,線程1執行完A後把結果給線程2,線程2開始執行B,完成後把B的結果通知到線程1,線程1開始執行C,線程1在等待操做B結果的時候執行了D,這就是一個異步的過程;此外,異步過程當中,B和D是並行執行的。網絡
並行會提升業務的執行效率,但異步不會,異步甚至會拖慢業務的執行,好比上面A->B->C的執行過程。異步是讓等待變得更有價值,這種價值則體如今多個業務的並行上。異步
在須要長時間等待的地方均可以使用異步,好比讀寫文件、訪問網絡或者處理圖片。特別是在UI線程中,咱們要保持界面的響應性,耗時的操做最好都使用異步的方式執行。async
.NET提供了三種異步模式:異步編程
其中基於任務的異步模式是.NET推薦的異步編程方式。函數
下面是IAsyncResult基於委託的用法。線程
/// <summary> /// 作做業的委託 /// </summary> /// <param name="workNo">做業編號</param> private delegate void AsyncWorkCaller(int workNo); public static void Run() { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work."); AsyncWorkCaller caller = DoWork; AsyncCallback callback = ar => {// 異步任務完成後的回調,在異步任務的執行線程中執行 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} did the callback. [{ar.AsyncState}]"); }; IAsyncResult result = caller.BeginInvoke(1, callback, "callback msg"); DoWork(2); //result.AsyncWaitHandle.WaitOne(); caller.EndInvoke(result); DoWork(3); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work."); } /// <summary> /// 作做業 /// </summary> /// <param name="workNo">做業編號</param> private static void DoWork(int workNo) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}."); Thread.Sleep(1000);//模擬耗時 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}."); }
咱們使用BeginInvoke
來異步執行做業1,同時能夠執行做業2,調用EndInvoke
的時候,當前線程被阻塞直到做業1完成。咱們也可使用result.AsyncWaitHandle.WaitOne()
來等待異步做業完成,一樣會阻塞當前線程。此外,能夠爲異步做業增長回調,異步做業在完成時會執行回調函數。code
事件你們不會陌生,咱們在Winform編程的時候,總會用到事件。下面是利用BackgroundWorker
實現的一個基於事件的簡單異步過程。咱們給異步對象(這裏是BackgroundWorker)訂閱DoWork
和RunWorkCompleted
事件,當調用RunWorkerAsync
時,觸發異步對象的工做事件,此時會開闢一個新線程來執行目標操做。目標操做完成時,觸發工做完成事件,執行後續操做。與IAsyncResult
模式不一樣的是,做業完成後的後續操做會在另外的一個線程執行,而IAsyncResult
模式中,完成回調會在目標操做的執行線程中執行。orm
public static class EventBasedAsync { private static readonly BackgroundWorker worker = new BackgroundWorker(); static EventBasedAsync() { worker.DoWork += Worker_DoWork; worker.RunWorkerCompleted += Worker_RunWorkerCompleted; } public static void Run() { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work."); worker.RunWorkerAsync(1); DoWork(2); DoWork(3); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work."); } private static void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {//做業完成後,會開闢新的線程執行指定的操做 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} did something when work completed."); } private static void Worker_DoWork(object sender, DoWorkEventArgs e) {//做業會運行在新的線程裏 DoWork((int)e.Argument); } /// <summary> /// 作做業 /// </summary> /// <param name="workNo">做業編號</param> private static void DoWork(int workNo) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}."); Thread.Sleep(3000);//模擬耗時 Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}."); } }
實際上,咱們能夠利用AsyncOperationManager
實現本身的異步對象,可使用dnSpy對BackgroundWorker
進行反編譯觀察具體的實現過程。
在《C#並行編程(4):基於任務的並行》中,咱們已經總結過Task
和Task<T>
的用法,這裏主要關注的是C#的async/await
語法與Task
的結合用法。
在C#中,咱們使用async標記定義一個異步方法,使用await來等待一個異步操做。簡單的用法以下:
public async Task DoWorkAsync() { await Task.Delay(1000); } public async Task<int> DoWorkAndGetResultAsync() { await Task.Delay(1000); return 1; }
用async/await
編寫異步過程很方便,但異步方法的執行過程是怎樣呢?下面的例子展現了一個異步操做的調用過程,咱們以這個例子來分析異步方法的調用過程。
public static async Task Run() { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} will do some work."); Task workTask1 = DoWork(1); // 不使用await調用的異步方法,與正常方法同樣 //await workTask1; Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask1.Id} by async call."); Task workTask2 = DoWork(2); await workTask2; Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask2.Id} by async call."); Task workTask3 = DoWork(3); await workTask3; Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} got task #{workTask3.Id} by async call."); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work."); } /// <summary> /// 異步做業 /// </summary> /// <param name="workNo">做業編號</param> /// <returns>異步任務</returns> private static async Task DoWork(int workNo) { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} started with thread #{Thread.CurrentThread.ManagedThreadId}."); DateTime now = DateTime.Now; await Task.Run(() => { Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} was running by task #{Task.CurrentId} with thread #{Thread.CurrentThread.ManagedThreadId}."); while (now.AddMilliseconds(3000) > DateTime.Now) {// 模擬計算過程 } }); Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> work #{workNo} done with thread #{Thread.CurrentThread.ManagedThreadId}."); }
先來看一下例子的輸出:
19:07:33.032779=> thread #10 will do some work.
19:07:33.039762=> work #1 started with thread #10.
19:07:33.075664=> thread #10 got task #2 by async call.
19:07:33.075664=> work #2 started with thread #10.
19:07:33.078658=> work #2 was running by task #3 with thread #11.
19:07:33.082647=> work #1 was running by task #1 with thread #6.
19:07:36.040739=> work #1 done with thread #6.
19:07:36.077638=> work #2 done with thread #11.
19:07:36.077638=> thread #11 got task #4 by async call.
19:07:36.077638=> work #3 started with thread #11.
19:07:36.077638=> thread #11 got task #7 by async call.
19:07:36.077638=> thread #11 done the work.
19:07:36.077638=> work #3 was running by task #6 with thread #12.
19:07:39.077652=> work #3 done with thread #12.
在上面的輸出中,咱們單看work #1,它由thread #10啓動,計算過程在thread #6中執行並結束,最後任務在thread #10中返回,這裏咱們沒有使用await
來等待work #1的異步任務;假如咱們使用await
等待異步任務,如work #2,它在thread #10中啓動,計算過程在thread #11中執行並結束,任務最後在thread #11中返回。你們可能發現了二者的不一樣:await
改變了Run()
方法的執行線程,從DoWork()
方法的執行也可以看出,await
會改變異步方法的執行線程!
實際上,編譯器會把異步方法轉換成狀態機結構,執行到await
時,編譯器把當前正在執行方法(任務)掛起,當await的任務執行完成時,編譯器再恢復掛起的方法,因此咱們的輸出中,異步方法await
前面和後面的代碼,通常是在不一樣的線程中執行的。編譯器經過這種狀態機的機制,使得等待異步操做的過程當中線程再也不阻塞,進而加強響應性和線程利用率。
理解異步方法的執行機制後,相信對異步的應用會變得更加嫺熟,這裏就再也不總結異步的具體用法。