.net core 非阻塞的異步編程 及 線程調度過程
本文主要分為三個部分:
1、語法格式
2、線程調度情況
3、編程注意事項
4、練一練
* 閱讀提示 :鼠標懸停在 章節標題 上可見 文章目錄
異步編程(Task Asynchronous Programming,TAP),一種編程模式(Task-based Asynchronous Pattern)。
TAP 是 .NET 中推薦的異步編程模式,基于 Task 和 Task<TResult> 類型,用于表示異步。
異步編程一般應對兩種場景,一是 I/O 綁定,當需要網絡連接(連接數據庫或讀寫到文件系統等)等耗時長的任務;二是 CPU 綁定,需要耗時長的計算。
1、簡單的語法格式
.net 一直在為開發人員簡化和標準化異步的寫法,從 .net 4.5 開始就已經支持使用 aysnc 和 await 的關鍵字。
異步的語法格式如下:
private async Task<TResult> DoSomeStuffAsync(..)
{
..
await ..
..
}
l 關鍵詞 async 本身不具備什么意義,只是裝飾,當方法冠以 async 關鍵詞,方法體內允許使用 await
l await 是標記需要等待的地方,但其本質并非阻塞線程。
l “非阻止操作”:指當運行到 await 時,會把當前線程返回到上一級調用者繼續執行,如果沒有上一級調用者,則該線程當場釋放。
|
|
非阻止操作 |
阻止操作 |
備注 |
| 獲取任務返回 | await task | task.Wait / task.Result |
非阻塞:線程遇到 await 時會返回上一層調用者繼續執行,如果沒有上一級調用者,則釋放該線程; 阻塞:線程在等待期間不能執行其他任務,也不釋放線程,硬等
|
|
任一任務完成 |
await Task.WhenAny |
Task.WaitAny |
|
|
所有任務完成 |
await Task.WhenAll |
Task.WaitAll |
|
|
等待一段時間 |
await Task.Delay |
Thread.Sleep |
l 異步方法返回的值總是 Task 的實例,可以是 Task 類型或 Task<TResult>,其中 TResult 是執行的方法的返回類型
l 如果直接拿異步方法的結果,形如 var obj = GetSomethingAsync(),這個 obj 是一個 Task 對象,其中 obj.AsyncStatus,obj.Result 可見執行情況
l await + 執行異步方法,形如 await GetSomethingAsync() 會得到 TResult 實例
l 直接使用 task.Result 得到的任務結果其狀態是未知的,應該使用 await task,保證任務是 Completed 的
l 一般地,異步方法的名字需要添加后綴 Async,以便于寫代碼的時候區分開同步方法和異步方法
l 等待異步任務的執行過程中,如果其中發生了錯誤,該異步任務的外層 try catch 會捕捉到,它也是一種任務結果
2、異步運行機制,線程調度
觀察以下代碼,思考一下控制臺會輸出什么?
public static async Task Main(string[] args)
{
logMessage("Main <<----");
var task = GetUrlContentLengthAsync();
logMessage("Main ---->>");
await task;
logMessage("ALL COMPLETED");
}
static async Task<int> GetUrlContentLengthAsync()
{
logMessage("GetUrlContentLengthAsync start ");
using var client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("https://learn.microsoft.com/dotnet");
// Do some independent work..
var contents = await getStringTask;
logMessage("GetUrlContentLengthAsync end ");
return contents.Length;
}
static void logMessage(string msg)
{
Console.WriteLine($"{DateTime.Now.ToString("MM-dd HH:mm:ss.fff")} [{Thread.CurrentThread.ManagedThreadId}] {msg}");
}
打印結果:
11-18 18:34:34.563 [1] Main <<---- 11-18 18:34:34.632 [1] GetUrlContentLengthAsync start 11-18 18:34:34.810 [1] Main ---->> 11-18 18:34:37.283 [7] GetUrlContentLengthAsync end 11-18 18:34:37.286 [7] ALL COMPLETED
先分析一下 GetUrlContentLengthAsync 這個異步方法,簡單歸納會存在以下步驟:
- httpClient.GetStringAsync 發起 HTTP 請求,并立即返回一個未完成的任務。
- await 關鍵字會暫停 GetStringAsync 方法的執行,并將控制權返回給調用方。
- 任務調度器通過操作系統的通知機制來監聽 HTTP 請求的響應。
- 操作系統在后臺監控 I/O 操作的狀態,并在操作完成時通知應用程序。
- 任務調度器隨后選擇一個可用的線程來繼續執行 異步方法的剩余部分(await 之后)。
線程運行過程如下:

* HTTP 請求是一個 I/O 操作,操作系統會通過 I/O 完成端口(IOCP)來處理這些操作。
* IOCP 是一種高效的機制,用于處理異步 I/O 操作。它允許操作系統在 I/O 操作完成時通知應用程序。
3、注意事項
l 如果調用了 異步方法,一定要 await 任務執行結果
反面示例:
public static async Task Main(string[] args)
{
logMessage("Main <<----");
// fault example: if do not wait the result then we will do not know what happened on it
GetSomeStuff();
logMessage("Main ---->>");
}
這個異步任務已經在執行,但是卻沒有后續處理,不知道是成功或是失敗,又或者一直在執行沒有辦法停止,這是危險的。
l 避免使用 task.Result,應該使用 await
| 使用 await,而避免使用 xxTask.Result 阻塞線程:如果任務尚未完成,訪問 Result 會阻塞當前線程,直到任務完成。這會導致性能問題,特別是在 UI 線程中使用時,會導致界面卡頓。 死鎖風險:在某些情況下,特別是在同步上下文(如 UI 線程)中,訪問 Result 可能會導致死鎖(任務等待當前線程釋放,而當前線程又在等待任務完成)。 異常處理:直接訪問 Result 可能會忽略任務中的異常。使用 await 可以更好地處理異常。 |
l 是否線程安全
異步編程的機制,允許到正在處理一個請求時,同時存在多個線程在處理操作,如果在對同一個對象做寫入操作,這是危險的。
所以并行時的任務最好是沒有關系的。
如果使用鎖,需要考慮是否會導致死鎖的問題。
l 異步編程可能會增加代碼復雜度,慎重取舍
異步編程并不一定帶來性能提升,畢竟上下文切換也是有開銷的,對于簡單的任務可能一條線做完的方式更合適。
4、練一練
以下代碼有什么問題?
public async Task<ResultResponse> ValidateReceiptAsync(List<Guid> customerIds, ReceiptRequest request, CancellationToken cancellationToken)
{
var tokenTask = _medicalCheckServiceHelper.GetServiceToken(cancellationToken);
var medicalProvidersTask = _medicalProviderRepo.GetMedicalProvidersNames();
var customersTask = _customerService.GetByIdsAsync(customerIds);
var checkResults = await Task.WhenAll(request.UuidList.Select(uuid => _medicalCheckServiceHelper.GetResultAsync(uuid, tokenTask.Result, cancellationToken)));
return await ConsolidateReceiptsResult(medicalProvidersTask.Result, checkResults, await customersTask);
}
思考一下
- 1. tokenTask.Result 在任務完成之前訪問會導致阻塞,應該在 await tokenTask 之后再訪問。
- 2. 確認哪些可以并行處理,注意線程安全
這里有 4 個任務,分別是 獲取 token(假作 taskA)、獲取治療廠商(taskB)、獲取用戶信息(taskC)、獲取檢查結果(taskD)
其中,taskD 依賴于 taskA ;現在需要確定 taskA、taskB、taskC 之間的依賴關系。
-
- a. 假設真實場景如下:
- i. taskA 取自于配置中心,也即它會走 HTTP 請求
- ii. taskD 是一個第三方接口,也即一個 HTTP 請求
- iii. taskB 和 taskC 取自同一個數據庫,并且它們共用了一個數據庫連接的上下文
- a. 假設真實場景如下:
那么可能的修改是這樣的:
public async Task<ResultResponse> ValidateReceiptAsync(List<string> customerIds, ReceiptRequest request, CancellationToken cancellationToken)
{
var tokenTask = _medicalCheckServiceHelper.GetServiceToken(cancellationToken);
var medicalProvidersTask = _medicalProviderRepo.GetMedicalProvidersNames();
var token = await tokenTask;
var checkResults = await Task.WhenAll(request.UuidList.Select(uuid => _medicalCheckServiceHelper.GetResultAsync(uuid, token, cancellationToken)));
var medicalProviders = await medicalProvidersTask;
var customers = await _customerService.GetByIdsAsync(customerIds);
return await ConsolidateReceiptsResult(medicalProviders, checkResults, customers);
}
-
- b. 假設真實場景如下:
- i. taskA 取自于配置中心,也即它會走 HTTP 請求
- ii. taskD 是一個第三方接口,也即一個 HTTP 請求
- iii. taskB 和 taskC 取自不同數據庫或不同的 HTTP 請求,它們相互獨立
- b. 假設真實場景如下:
那么可能的修改是這樣的:
public async Task<ResultResponse> ValidateReceiptAsync(List<string> customerIds, ReceiptRequest request, CancellationToken cancellationToken)
{
var tokenTask = _medicalCheckServiceHelper.GetServiceToken(cancellationToken);
var medicalProvidersTask = _medicalProviderRepo.GetMedicalProvidersNames();
var customersTask = _customerService.GetByIdsAsync(customerIds);
var token = await tokenTask;
var checkResultsTask = Task.WhenAll(request.UuidList.Select(uuid => _medicalCheckServiceHelper.GetResultAsync(uuid, token, cancellationToken)));
var medicalProviders = await medicalProvidersTask;
var customers = await customersTask;
var checkResults = await checkResultsTask;
return await ConsolidateReceiptsResult(medicalProviders, checkResults, customers);
}
但其實并不盡完善,因為這種寫法的可讀性并沒有那么好,看起來更追求資源優化。
所以說引入了 異步編程 的話,代碼是會變復雜的,每寫一步都需要慎重考慮。
Reference:
[1] The Task Asynchronous Programming (TAP) model with async and await" - C# | Microsoft Learn
[2] Asynchronous programming scenarios - C# | Microsoft Learn
[3] Asynchronous programming - C# | Microsoft Learn

浙公網安備 33010602011771號