C#并行编程(5):需要知道的异步
異步與并行的聯系
大家知道“并行”是利用CPU的多個核心或者多個CPU同時執行不同的任務,我們不關心這些任務之間的依賴關系。
但是在我們實際的業務中,很多任務之間是相互影響的,比如統計車間全年產量的運算要依賴于各月產量的統計結果。假如你想在計算月產量的時候做些其他事情,如導出生產異常報表,“異步”就可以登上舞臺了。
說到異步,必須要先提一下同步。一圖勝千言:
圖中操作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的執行過程。異步是讓等待變得更有價值,這種價值則體現在多個業務的并行上。
C#中的異步
在需要長時間等待的地方都可以使用異步,比如讀寫文件、訪問網絡或者處理圖片。特別是在UI線程中,我們要保持界面的響應性,耗時的操作最好都使用異步的方式執行。
.NET提供了三種異步模式:
IAsyncResult模式(APM)
基于事件的異步模式(EAP)
基于任務的異步模式(TAP)
其中基于任務的異步模式是.NET推薦的異步編程方式。
IAsyncResult異步模式APM
下面是IAsyncResult基于委托的用法。
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);
caller.EndInvoke(result);
DoWork(3);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.ffffff}=> thread #{Thread.CurrentThread.ManagedThreadId} done the work.");
}
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()來等待異步作業完成,同樣會阻塞當前線程。此外,可以為異步作業增加回調,異步作業在完成時會執行回調函數。
基于事件的異步模式EAP
事件大家不會陌生,我們在Winform編程的時候,總會用到事件。下面是利用BackgroundWorker實現的一個基于事件的簡單異步過程。我們給異步對象(這里是BackgroundWorker)訂閱DoWork和RunWorkCompleted事件,當調用RunWorkerAsync時,觸發異步對象的工作事件,此時會開辟一個新線程來執行目標操作。目標操作完成時,觸發工作完成事件,執行后續操作。與IAsyncResult模式不同的是,作業完成后的后續操作會在另外的一個線程執行,而IAsyncResult模式中,完成回調會在目標操作的執行線程中執行。
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);
}
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進行反編譯觀察具體的實現過程。
基于任務的異步模式TAP
在《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);
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.");
}
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前面和后面的代碼,一般是在不同的線程中執行的。編譯器通過這種狀態機的機制,使得等待異步操作的過程中線程不再阻塞,進而增強響應性和線程利用率。
理解異步方法的執行機制后,相信對異步的應用會變得更加嫻熟,這里就不再總結異步的具體用法。
原文地址:https://www.cnblogs.com/chenbaoshun/p/10671403.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結
以上是生活随笔為你收集整理的C#并行编程(5):需要知道的异步的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【西安活动】 | 4月20日「拥抱开源,
- 下一篇: 从壹开始 [ Id4 ] 之一║ 授权服