Home [C# 筆記] 異步 async、await
Post
Cancel

[C# 筆記] 異步 async、await

什麼是異步

異步(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」。

關於程式的運行順序,先按下不表。來說說這裡幾處關鍵的程式變動。

  1. 添加 async 關鍵字

添加 async 關鍵字的目的在於,將方法明示為一個異步方法,從而在其內部的 await 單詞會被識別為一個關鍵字,要使用 await語句,必須在方法中加入 async關鍵字。

  1. 對 DoSomething 方法的調用前添加 await關鍵字

await 是異步編程的靈魂,用於等待一個「可等待」(awaitable)的對象返回值,同時向異步方法的調用者返回控制育。這裡,我們使用 Task對象來實現計算任務。

  1. 將計算任務的返回值更改為 Task

這裡的含義是「返回值類別型為字串的任」。Task 本身是可等待的對象,因而可以作為 await關鍵字操作的要素。這個方法是 await要等待的任務,它本身是不需要用 async 關鍵字來修飾的。

  1. 建立新線程(新執行緒)完成具體工作

(1) 用Task.Run方法直接將 t 定義為一個新的 Task,並且立刻執行。由於 Task 本身是利用執行緒池(Thread Pool)在後端執行的,所以這一步是實現異步編程多執行緒的核心。當我們撰寫自己的異步實現方法(注意不是異步方法)時要進行多線程(多執行緒)的操作,否則程式始終還是同步(按順序)執行的。

(2) 變數t作為返回值,必須與方法相同,是 Task類型的,但在Task.Run中並沒有體現,而是在參數中的 Lambda 表達式所體現的,因 Lambda表達式程式區塊中返回了一個字串。實際上,也可以明確地將 t定義為 Task.Run。

  1. 返回變數 t

異步實現方法DoSomething的返回值類型是 Task,為什麼在調用方法中由類型為 string 的變數接收返回值呢?這是由異步編程模型和Taks模型內部的邏輯所決定的。

如此,我們就實現了一個簡單的異步編程,不僅包含了編寫異步方法,也包含了編寫異步實現方法。這可能是我個人的說法:異步方法的方法名要有 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;
}

這樣,我們就可以十分方便地實現異步編程,無序大量的多線程處理,就可以實現後端工作和前端響應兩不誤。

或者,可以編寫自己的異步實現方法,用來實現異步調用,如同上文的例子一樣。

This post is licensed under CC BY 4.0 by the author.