非同步與並行的聯絡
大家知道“並行”是利用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
的用法,這裡主要關註的是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
前面和後面的程式碼,一般是在不同的執行緒中執行的。編譯器透過這種狀態機的機制,使得等待非同步操作的過程中執行緒不再阻塞,進而增強響應性和執行緒利用率。
理解非同步方法的執行機制後,相信對非同步的應用會變得更加嫻熟,這裡就不再總結非同步的具體用法。
朋友會在“發現-看一看”看到你“在看”的內容