C#中關鍵字 async 和 await 的使用


C#中關鍵字 async 和 await 的使用

1. 背景知識點

(1)同步和異步

同步:相同的步速或步調。

在多線程編程中,異步就是:在當前線程之外,另開一個線程,以執行一個相對獨立的任務;當前線程不管新開線程是否執行完畢,繼續執行自身任務或結束自身。相反地,同步就是:當前線程等待新開線程執行完畢,再繼續執行自身任務【一個等待另一個的結束,在它結束之后,繼續自身】。

通俗地講,同步--調用方等待任務完成;異步--調用方不等待任務完成

(2)三種異步的詳細介紹及實現
  • .NET 三種異步的詳細介紹及實現查看微軟官方相關文檔
    • 異步編程模型 (APM,Asynchronous Programming Model) :

      通過在類中提供 public IAsyncResult BeginOperation()、public int EndOperation(IAsyncResult asyncResult) 兩個方法,來實現異步編程:在 BeginOperation 方法中新開線程【可以通過委托的BeginInvoke方法來開線程】,並返回 IAsyncResult 結果;在 EndOperation 方法中阻塞當前線程而等待新開線程結束,並返回新開線程的結果[例如,int]。

    • 基於事件的異步模式 (EAP,Event-based Asynchronous Pattern);

      先聲明一個委托,或不聲明委托而使用 .net 提供的委托;然后再定義一個 EventArg 類,或使用 .net 提供的EventArg 類;最后在類中聲明一個事件,並實現一個以 Async 為后綴的方法,在方法中適當的地方響應事件回調【例如,OnXXXCompleted()】。

    • 基於任務的異步模式 (TAP,Task-based Asynchronous Pattern) 。

      異步方法應返回 System.Threading.Tasks.Task 或 System.Threading.Tasks.Task 對象。
      使用手動方式實現 TAP 異步模式:需要創建一個 TaskCompletionSource 對象、執行異步操作,並在操作完成時,調用 SetResult、SetException、SetCanceled 方法。
      使用編譯器以實現 TAP 異步模式:使用 async 關鍵字,將方法交給編譯器進行執行必要的轉換,從而實現 TAP 異步模式。異步方法應返回 System.Threading.Tasks.Task<TResult> 對象時,函數的主體應返回 TResult。
      TAP 取代了 APM 和 EAP,遷移時請參考: TAP 與 APM、EAP 等的互相轉化
      與 TAP 類似的 TPL,更側重於並行處理,而 TAP 更側重於異步處理。

2. async 和 await 關鍵字的誕生

async,單詞原意:異步。

C# 中的 async 和 await 關鍵字,屬於基於任務的異步模式,是 .NET 對 TAP 在語言級別上的支持。通過這兩個關鍵字,可以輕松創建異步方法(我們可以像寫同步代碼一樣去寫異步代碼,幾乎與創建同步方法一樣輕松)。

  • async 和 await 是基於 Task 的;
  • Task 是對 ThreadPool 的封裝改進,主要是為了更有效的控制線程池中的線程(ThreadPool 中的線程,我們很難通過代碼控制其執行順序,任務延續和取消等等);
  • ThreadPool 基於 Thread 的,主要目的是減少 Thread 創建數量和管理 Thread 的成本。

net4.0 在 ThreadPool 的基礎上推出了 Task 類,微軟極力推薦使用 Task 來執行異步任務,現在 C# 類庫中的異步方法基本都用到了 Task;net5.0 推出了 async/await,讓異步編程更為方便。我們在開發中可以嘗試使用 Task 來替代 Thread/ThreadPool,處理本地 IO 和網絡 IO 任務時,盡量使用 async/await 來提高任務執行效率。觀點來源

誕生的目的和意義:通過 async 和 await 關鍵字,可以輕松創建異步方法,從而實現異步編程,比 APM、EAP 模式來得輕松、易讀和易維護,和 TAP 的其他方式 相比,也有同樣的優點。

3. 用法

關鍵字 async 用於標記函數,使編譯器知道該函數是一個異步方法,從而執行必要的轉換,以實現 TAP 異步模式。
關鍵字 await 用於獲取“被關鍵字 async 標記的函數”的調用結果。獲取結果時,會暫停自身所在函數的執行【因此,自身所在的函數也應該用關鍵字 async 標記】。

函數被 async 標記 函數未被 async 標記
函數體中含有 await 基於任務的異步模式 報錯,await 只能用於被 async 標記的函數中
函數體中沒有 await 警告,函數將以同步方式運行 普通函數

async 和 await 關鍵字的使用限制:

  • 被 async 標記函數,返回類型只能是:void、Task、Task<T>、IAsyncEnumerable<T>、IAsyncEnumerator<T> 以及類似任務的類型
    其中,IAsyncEnumerable<T> 或 IAsyncEnumerator<T> 是在.NET Core 3.0(.NET Standard 2.1)中引入的;
  • 程序入口 main 函數的返回類型:如果 main 函數中使用了 await 運算符,則它的返回類型只能是 Task 或 Task<int>,否則報錯:程序不包含適合於入口點的靜態 "Main" 方法。
  • 不能對返回類型是 void 的異步方法使用 await 運算符。

類似任務(Task-like)的類型:實現了 public GetAwaiter() 的類型,例如:System.Threading.Tasks.ValueTask<TResult> 類型,參考:《C#中的 Awaiter》。

4. 應用場景

根據“被 async 標記的函數”的返回類型,將應用場景分為以下五種:

調用方目的(應用場景) 異步方法的返回值類型 異步方法的返回語句
僅僅只是調用一下異步方法,不和異步方法做其他交互 Void 不需要 return 語句
不想通過異步方法獲取一個值,僅僅想追蹤異步方法的執行狀態 Task 不需要 return 語句
想通過調用異步方法獲取一個 T 類型的返回值 Task<T> return T 類型值
想通過調用異步方法獲取一個返回值;該返回值為枚舉,用於遍歷其中元素;該枚舉包含多個 T 類型元素 IAsyncEnumerable<T> (yield) return T 類型值
同上 IAsyncEnumerator<T> 同上

通俗的理解:

  • 返回類型 void:調用后不管任務的執行情況;
  • 返回類型 Task:調用后可以知道任務是否結束;
  • 返回類型 Task<T>:調用后可以拿到任務的結果;
  • 返回類型 IAsyncEnumerable<T>、IAsyncEnumerator<T>:調用后可以以枚舉的方式拿到任務的結果。
  • 返回類型 類似任務的類型:略。

5. 原理

在 C# 方面,編譯器將代碼轉換為狀態機,它將跟蹤類似以下內容:到達 await 時暫停執行以及后台作業完成時繼續執行。從理論上講,這是異步的承諾模型的實現。

6. 控制流流轉

(1)返回類型是 void 的異步方法

返回類型是 void 的異步方法,不能對它使用 await 運算符,須直接調用;這樣的調用,不阻塞調用方所在線程。

  • 異步方法中,第一個 await 之前的代碼,由調用方線程執行;
  • 如果調用方在“異步調用”之后還有代碼,由調用方所在線程立即執行(不阻塞)。
  • 異步方法中,各 await 語句,由實際的 Task 另開線程執行【此處“另開線程”是相對於調用方所在線程:無論物理上,還是邏輯上,都不是調用方所在線程】;
    而各 await 語句所開的線程,在邏輯上,是不同線程,在物理上,可能是同一個線程【前一個 await 語句所對應的線程已經執行完畢,后一個 await 語句將不再創建新的物理線程,而是復用前一個線程,以降低創建線程的開銷】;
  • 異步方法中,最后一個 await 之后如果有代碼,則由“最后完成任務”的線程來執行【未必是最后一個 await 語句所對應的線程】。當然,如果異步方法中只有一個 await 語句,則它就是最后一個 await 語句,必然是由其來執行之后的代碼;
  • 異步方法中,如果有多個 await,並且它們中間夾有其他代碼,這些代碼的執行情況是:
    從“第一個 await 所開線程”開始,由其執行“第一個和第二個await中間的代碼”,
    • A、執行完畢時檢查“第二個 await 所開線程”是否已經執行完畢:
      -> 如果已經執行完畢,則由“第一個 await 所開線程”繼續執行“第二個和第三個 await 中間的代碼”;
      -> 如果尚未執行完畢,則“第一個 await 所開線程”結束並歸還給線程池,然后等待“第二個await所開線程”執行完畢,並且之后,由“第二個await所開線程”來執行“第二個和第三個await中間的代碼”;
    • B、如此執行,每遇到一個 await 語句,都檢查其所對應線程是否已經執行完畢,如果沒有執行完畢,則線程結束並歸還給線程池,否則繼續執行。
示例一
internal partial class Program
{
	static void Main(string[] args)
	{
		//另開線程,用作“調用方線程”
		Task.Run(() =>
		{
			ToTest();
		});

		//主線程駐留內存
		Console.ReadKey();
	}
}

public partial class Test
{
	public static void PrintThreadInfo(string callTag)
	{
		//輸出當前線程Id,以及調用時給的標記
		Console.WriteLine($"Thread Id:{Environment.CurrentManagedThreadId} - {callTag}");
	}
}

新開線程:await 語句中,由 Task 另開的線程。
流程1
流程1結果

(2)返回類型是 Task 或 Task<T> 的異步方法

調用方對異步方法的調用方式,面臨 2 個選擇:

  • 一是直接調用:收到警告,但不報錯,同時控制流流轉方式與“返回類型是 void 的異步方法”的相同。
  • 二是在異步方法前加 await 關鍵字,如下圖示例二所示:
    • 調用方線程僅執行到異步方法中 第一個 await 語句;
    • 調用方 await 語句之后還有代碼,視為異步方法最后一個await語句之后的代碼,將由新開線程執行;
    • 其他的控制流流轉方式,與“返回類型是 void 的”控制流流轉方式一致。
示例二

流程2
流程2結果

7. 總結

使用關鍵字 async 和 await ,使開發者能更好地、簡便地進異步編程。

控制流流轉方式簡便記憶:

  • await 前,同線程(與調用方);
  • await 后,新線程(包括調用方調用后代碼);
  • 返回 void,不阻塞;
  • 多個 await,比誰活得久。

8. 結束語

如有錯誤,歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM