什麼是異步
異步(Asynchrony)就是「非同步」
在中文裡,「同步」給人起來的感覺就是「同時進行的事情」,例如「挖土和蓋樓兩件事情同步進行中」,表示的是我們在挖土的同時也在蓋大樓。然而在碼農的世界裡,「同步」的意思其實是「序貫執行」,說人話就是「一個接一個地有序執行」。例如你寫了5件事情在任務清單上,你必須得做完第一件再做第二件,順序不可跳躍。
這帶來一個問題:當上一個任務沒有完成時,下一個任務無法開始。那麼當你有30分鐘燒開水、10分鐘洗茶杯、10分鐘洗茶壺,10分鐘找到茶葉這四件事情要完成時,你無法先去把水燒上,等他燒開的同時,洗杯子,洗壺,找茶葉,只能傻等到水燒開了之後再去做剩下的三件事。如此一來,總共可以在30分鐘內完成的事情,拖拖拉拉到60分鐘才能搞定,實乃人力物力財力的巨大浪費。
程式「異步」的概念,其實意味著「同時進行」的事情,可能理解為「你去挖土,我來蓋樓」,如此我們「各做各的,分工相異,同時進行,完事爾後一起交差」。放到沏茶的例子裡,就是在燒開水的同時,去洗杯子,洗壺,找茶葉,總共30分鐘完成。
以使用者介面來說:WinForm 程式啟動時,只有一個線程(執行緒),即UI線程。此時所有的工作都是由UI線程來處理,當工作量較小時,一瞬間即可完成,用戶不會覺得有什麼異樣;而當假設完成一個巨型計算量的工作需要30分鐘時,這個線程就會拼命的不停地去計算這個結果,而無暇顧及用戶對UI的操作,導致UI卡死無回應。這種情況要極力避免的,任何時候都應當以向用戶提供實時操作反饋為第一目標,所以那些極其費計算資源的事情,應該扔到後端去做。
這聽起來像是「多線程(多執行緒)」?的確,異步其實是多線程編程的一種實現方法。與傳統方法相比,異步在程式寫法、實現方式、管理複雜度和異常處理方法更加便捷而高效,並且,異步程式的寫法,「看上去就像同步的程式一樣」,簡單而直接。當然了,異步的本質仍然逃不開多線程,無論是調用別人的異步方法,還是編寫自己的異步方法,都是要新開線程來完成工作的,單線程的異步,本質上是同步的。不過好在,異步的引入,使得這一過程得到了極大的簡化。
如何編寫異步程式
先建立對異步方法的認識:
- 異步方法回傳值類型有三種:void, Task, Task
。 - 使用 async 關鍵字修飾方法
- 在異步方法內使用 await 關鍵字來等待一個「可等待」類型,實現異步。
假設我們要實現這樣的功能:點擊一個按鈕,進行一個計算量巨大的操作,要耗時30秒,計算結束後在表單內顯示計算結果。程式如下:
1
2
3
4
5
6
7
8
9
private void button1_Click(object sender, EventArgs e)
{
var result = DoSomething();
label1.Text = result;
}
private string DoSomething() {
Thread.Sleep(30000);
return "result";
}
這裡將當前線程掛起30秒,來模擬耗時30秒的計算過程。很顯然,運行程式點擊按鈕後,UI會在30秒內毫無反應,全心地投入到複雜的計算過程中。(這時候連Form表單都是整個卡住凍結)
接下來我們用異步編程的方法來改善這一個問題。異步編程的核心思想是,當遇到 await關鍵字時,控制權立即返回調用者,同時等待 await語句所指向的異步方法的結束,當方法執行完畢返回結果時,接著執行await語句後面的代碼。
放在這裡的就是,當點擊按鈕時,我們要進行巨大耗時計算,此時我們希望控制權立刻返還給UI,使得UI可以相應用戶的其他操作,同時在後端進行計算工作,當得出計算結果時,我們把它顯示在表單上。
那麼就按照如下方法改造之前的程式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//給事件處理器添加 async 關鍵字
private async void button1_Click(object sender, EventArgs e)
{
//給計算方法的調用添加 await
label1.Text = await DoSomething();
}
//將返回值改成 Task<string>
private Task<string> DoSomething()
{
//將計算操作放到Task<string>中,新開線程
var t = Task.Run(() =>
{
//使用 lambda 表達式定義計算和返回值
Thread.Sleep(30000);
return "result";
});
return t; //返回這個task
}
現在再運行一遍,可以發現,點擊按鈕後計算開始運行,但是 UI仍然可以響應用戶的操作,例如對表單的移動、縮放,和點擊其他控件等等,30後,計算完成,表單上的Label出現結果「result」。
關於程式的運行順序,先按下不表。來說說這裡幾處關鍵的程式變動。
- 添加 async 關鍵字
添加 async 關鍵字的目的在於,將方法明示為一個異步方法,從而在其內部的 await 單詞會被識別為一個關鍵字,要使用 await語句,必須在方法中加入 async關鍵字。
- 對 DoSomething 方法的調用前添加 await關鍵字
await 是異步編程的靈魂,用於等待一個「可等待」(awaitable)的對象返回值,同時向異步方法的調用者返回控制育。這裡,我們使用 Task對象來實現計算任務。
- 將計算任務的返回值更改為 Task
這裡的含義是「返回值類別型為字串的任」。Task 本身是可等待的對象,因而可以作為 await關鍵字操作的要素。這個方法是 await要等待的任務,它本身是不需要用 async 關鍵字來修飾的。
- 建立新線程(新執行緒)完成具體工作
(1) 用Task.Run方法直接將 t 定義為一個新的 Task,並且立刻執行。由於 Task 本身是利用執行緒池(Thread Pool)在後端執行的,所以這一步是實現異步編程多執行緒的核心。當我們撰寫自己的異步實現方法(注意不是異步方法)時要進行多線程(多執行緒)的操作,否則程式始終還是同步(按順序)執行的。
(2) 變數t作為返回值,必須與方法相同,是 Task
- 返回變數 t
異步實現方法DoSomething的返回值類型是 Task
如此,我們就實現了一個簡單的異步編程,不僅包含了編寫異步方法,也包含了編寫異步實現方法。這可能是我個人的說法:異步方法的方法名要有 async 關鍵字,在方法體中要有 await 關鍵字,用來執行異步操作的方法;而異步實現方法就是,返回值類型為可等待的,由多線程(多執行緒)來執行具體任務的方法。
在.NET4.5中,微軟提供了一批己經預先編寫好的異步實現方法,例如 HttpClient 對象的 GetStringAsync 方法,其返回值是 Task
1
2
3
4
5
6
7
using System.Net.Http;
......
private async void button1_Click(object sender, EventArgs e)
{
var result = await new HttpClient().GetStringAsync("about:blank");
label1.Text = result;
}
這樣,我們就可以十分方便地實現異步編程,無序大量的多線程處理,就可以實現後端工作和前端響應兩不誤。
