第十四章:深度解密 async/await 與 Task 的底層原理
第十四章:深度解密 async/await 與 Task 的底層原理
- 第十四章:深度解密 async/await 與 Task 的底層原理
- 14.1 引言:從回調地獄到 async/await
- 14.2 Task 到底是什么?
- 14.3 async/await 的編譯與運行
- 1. async/await 是什么?
- 2. async/await 的編譯與執行流程
- 3.
AsyncTaskMethodBuilder<T>.AwaitUnsafeOnCompleted源碼解讀- 完整源碼
- 解讀
- 整體概述
- 分步解析
- 1. 頂層方法:
AwaitUnsafeOnCompleted - 2. 內部實現:
AwaitUnsafeOnCompleted - 3. 包裝狀態機:
GetStateMachineBox - 4. 注冊回調:
AsyncTaskMethodBuilderT的AwaitUnsafeOnCompleted重載方法 - 5. 注冊回調:
TaskAwaiter的UnsafeOnCompletedInternal - 6. 包裝回調對象并將其添加到任務延續中:
Task.UnsafeSetContinuationForAwait - 7. 注冊回調到任務中:
AddTaskContinuation - 8. 多回調處理:
AddTaskContinuationComplex
- 1. 頂層方法:
- 總結與重點
- 4. await 的核心
- 14.4 自定義Task、async/await實現
- 14.5 async/await 為什么這么好用?
- 14.6 async/await 與同步上下文的協作
- 14.7 async/await 與執行上下文的協作
- 14.8 AsyncLocal 與異步編程中的數據流轉
- 14.9 async/await 與 Task 的常見誤區
- 14.10 async/await 的性能優化與高級用法
14.1 引言:從回調地獄到 async/await
在現代軟件開發中,異步編程是解決高并發、響應式交互和 I/O 密集型任務的重要方式。然而,早期的異步編程模型卻充滿了復雜性和陷阱。從最初的回調函數,到基于事件的異步模式(EAP),再到任務并行庫(TPL)的引入,異步編程的歷史是一段不斷演進的旅程。最終,async/await 的出現被稱為“異步編程的革命”,它以同步代碼的形式實現了異步邏輯的表達,大幅提升了代碼的可讀性與可維護性。
1. 從回調函數到事件驅動:早期的異步編程模型
1.1 回調函數(Callback)的局限性
回調函數是最早用于異步編程的解決方案,其核心思想是:當一個異步操作完成后,調用一個預定義的函數來處理結果。例如,以下是一個典型的基于回調的異步操作:
void FetchData(string url, Action<string> callback)
{
// 模擬異步操作
ThreadPool.QueueUserWorkItem(_ =>
{
string data = $"Data from {url}";
callback(data);
});
}
// 使用回調
FetchData("https://example.com", result =>
{
Console.WriteLine($"Result: {result}");
});
雖然回調函數簡單直觀,但它有以下幾個顯著問題:
-
回調地獄(Callback Hell):
當多個異步操作需要按順序執行時,回調函數會形成復雜的嵌套結構,導致代碼難以閱讀和維護。例如:fetchData((result1) => { // 拉取數據 ProcessDataAsync(result1, (result2) => { // 處理數據 saveData(result2, (result3) => { // 保存數據 console.log("All operations complete"); }); }); });這種代碼結構不僅讓程序員難以理解,還容易引入錯誤。
-
錯誤處理困難:
異常必須通過回調顯式傳遞,而不能使用傳統的try/catch進行捕獲。例如:fetchData((result, error) => { if (error) { console.error("Error:", error); } else { console.log(result); } }); -
可讀性和可維護性差:
回調函數讓代碼邏輯變得瑣碎且冗長,增加了調試和測試的復雜性。
1.2 基于事件的異步模式(EAP)
為了改善回調函數的局限性,.NET 提出了基于事件的異步模式(Event-based Asynchronous Pattern, EAP)。EAP 的核心思想是:通過事件通知異步操作的完成,并允許開發者注冊事件處理程序。例如:
FileDownloader downloader = new FileDownloader();
downloader.DownloadCompleted += (sender, e) =>
{
if (e.Error == null)
{
Console.WriteLine("Download completed: " + e.Result);
}
else
{
Console.WriteLine("Error: " + e.Error.Message);
}
};
downloader.DownloadAsync("http://example.com/file");
雖然 EAP 改善了回調函數的嵌套問題,但它仍然存在顯著的缺點:
-
事件管理復雜:
需要手動管理事件訂閱和取消,可能導致內存泄漏(如事件未被正確解除訂閱)。 -
錯誤處理分散:
異常仍然需要通過事件參數傳遞,不能直接使用try/catch。 -
代碼結構分散:
邏輯代碼分布在事件處理程序中,仍然難以維護。
1.3 任務并行庫(TPL)和 Task 的引入
為了解決回調和 EAP 的問題,.NET 在 .NET Framework 4 中引入了 任務并行庫(Task Parallel Library, TPL),核心是 Task 類型。Task 將異步操作的結果封裝為對象,并提供了更強大的功能,如鏈式調用和統一的異常處理機制。
以下是使用 Task 的示例:
Task.Run(() =>
{
return DownloadFile("http://example.com/file");
}).ContinueWith(task =>
{ // 處理后續任務
if (task.IsFaulted)
{
Console.WriteLine("Error: " + task.Exception.InnerException.Message);
}
else
{
Console.WriteLine("Download completed: " + task.Result);
}
});
優點:
-
支持鏈式調用:
使用ContinueWith可以將多個異步操作串聯起來,避免了嵌套的回調地獄。 -
統一的異常處理:
異常被封裝在Task.Exception中,支持統一的處理方式。 -
更強的靈活性:
支持并發任務的管理(如Task.WhenAll和Task.WhenAny)。
局限性:
- 雖然
Task改善了代碼的結構,但依然存在一定的復雜性,尤其是在處理多個嵌套任務時。
2. async/await:優雅地解決回調地獄
2.1 async/await 的設計哲學
async/await 是基于 Task 的進一步封裝,旨在解決異步編程中的可讀性和維護性問題。它允許開發者以同步的編碼風格編寫異步代碼,同時避免了回調地獄和復雜的任務鏈式調用。
以下是一個使用 async/await 的示例:
public async Task DownloadFilesAsync()
{
try
{
string result = await DownloadFileAsync("http://example.com/file");
Console.WriteLine("Download completed: " + result);
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
特點:
-
同步風格的代碼:
代碼從上到下順序執行,邏輯清晰,避免了嵌套和分散。 -
內置異常處理:
異常可以通過try/catch捕獲,不需要額外的事件處理或錯誤回調。 -
與
Task無縫集成:
async/await是對Task的擴展,完全兼容已有的任務并行庫。
2.2 async/await 如何優雅地解決回調地獄?
-
摒棄嵌套:
使用await可以直接等待異步操作的完成,而不需要嵌套回調。例如:// 回調地獄示例: FetchData((data) => { ProcessDataAsync(data, (processed) => { SaveData(processed, (saved) => { Console.WriteLine("All done!"); }); }); }); // 使用 async/await: async Task DoWorkAsync() { var data = await FetchDataAsync(); var processed = await ProcessDataAsyncAsync(data); await SaveDataAsync(processed); Console.WriteLine("All done!"); } -
邏輯清晰:
通過await,異步代碼的執行順序變得更加直觀,更接近同步代碼的風格。 -
錯誤處理簡單:
不需要手動訂閱錯誤回調或檢查異常狀態,try/catch即可處理所有異常。
3. async/await:異步編程的革命
3.1 更易讀、更易維護
-
同步化的異步代碼:
使用async/await,開發者可以以同步的方式組織異步邏輯,大幅提升代碼的可讀性和可維護性。 -
錯誤處理統一:
異步方法的異常可以通過try/catch捕獲,與同步方法無異。 -
消除嵌套:
不再需要嵌套的回調函數或復雜的任務鏈式調用。
3.2 更高效的資源利用
-
非阻塞模型:
await的本質是掛起當前方法,釋放線程資源,等待異步操作完成后繼續執行。 -
線程池的高效利用:
async/await 避免了傳統模型中線程的空閑等待,提升了資源利用率。
總結
從回調函數到 EAP,再到 TPL 和 async/await,異步編程經歷了從復雜到優雅的演變過程。async/await 被稱為“異步編程的革命”,因為它讓開發者以同步代碼的風格編寫異步邏輯,大幅提升了代碼的可讀性和維護性,同時充分利用了系統資源。它的出現,標志著異步編程進入了一個全新的時代。
14.2 Task 到底是什么?
在 .NET 的異步編程模型中,Task 是一個核心概念。它是任務并行庫(Task Parallel Library, TPL)的基礎,也是 async/await 語法的基石。盡管 Task 經常被用來處理異步操作,但它并不是一個簡單的線程,而是一個更高級的抽象。為了深入理解 Task,我們需要剖析它的角色、核心組件以及不同的類型。
1. Task 的角色
1.1 Task 是線程嗎?為什么它不是線程?
很多人初學時會誤認為Task直接與線程關聯,但實際上,Task 是一個 異步操作的抽象,并不直接映射到任何具體的線程。。以下是 Task 和線程的本質區別:
-
線程的本質:
- 線程是操作系統分配 CPU 時間的基本單位,每個線程都有自己的堆棧和上下文。
- 線程的生命周期由操作系統管理,線程的創建和銷毀開銷較大。
-
Task 的本質:
Task是一個邏輯任務的抽象,可以代表任意的異步工作(如 I/O 操作、計算任務)。Task并不直接創建線程,而是可能在某個線程上運行,大多數都在線程池線程上執行,也可能不依賴線程(例如等待 I/O 操作時)。Task的調度由TaskScheduler管理,線程池中的線程會被復用以減少開銷。
總結:
Task 是一個輕量化的、面向邏輯的異步操作的容器,它的實現與線程解耦,但可以利用線程池中的線程來執行任務。通過這種設計,Task 提供了更高效的資源管理和靈活性。
1.2 Task 是如何管理異步操作狀態的?
Task 的核心功能是管理異步操作的狀態和結果。內部有一個狀態機,能夠跟蹤任務的生命周期,并提供相關的狀態信息。這些狀態包括:
- Pending(等待中): 任務尚未開始執行。
- Running(運行中): 任務正在執行。
- Completed(已完成): 任務成功完成。
- Faulted(出錯): 任務執行過程中發生了未處理的異常。
- Canceled(已取消): 任務被取消。
Task 提供了以下機制來管理狀態:
-
狀態查詢:
可以通過Task.Status屬性查看任務的當前狀態。 -
結果管理:
通過Task.Result獲取任務的返回值(如果任務沒有完成,會阻塞調用線程)。 -
錯誤處理:
通過Task.Exception獲取任務中未捕獲的異常。 -
取消支持:
通過CancellationToken支持任務的取消操作。
Task 通過內部狀態機實現這些狀態管理,并暴露了 IsCompleted、IsFaulted 等屬性方便開發者檢查狀態。
Task 的這種狀態管理機制,使得異步操作的執行過程變得透明且易于跟蹤。
2. Task 的核心組件
2.1 Task 的生命周期
一個 Task 的生命周期可以分為以下幾個階段:
-
創建:
使用Task或Task.Run創建一個任務,但此時任務尚未開始。Task task = new Task(() => Console.WriteLine("Hello, Task!")); -
調度:
調用Task.Start()或直接使用Task.Run()會將任務提交到任務調度器中。task.Start(); // 或者直接使用 Task.Run() -
運行:
任務開始執行,進入Running狀態。 -
完成:
如果任務成功執行完畢,進入Completed狀態。 -
取消或失敗:
如果任務被取消或發生未處理的異常,分別進入Canceled或Faulted狀態。try { await task; } catch (Exception ex) { Console.WriteLine($"Task failed: {ex.Message}"); }
2.2 TaskScheduler 與線程池的關系
Task 的執行依賴于 TaskScheduler,它負責將任務分配到合適的線程中。默認情況下,TaskScheduler 使用線程池(ThreadPool)來調度任務。
更詳細內容參考第十三章:調度
-
線程池的作用:
- 線程池是一個線程復用機制,可以避免頻繁創建和銷毀線程的開銷。
- 線程池中的線程是動態分配的,能夠根據系統負載調整線程數量。
-
TaskScheduler 的作用:
TaskScheduler是一個抽象類,定義了任務的調度邏輯。- 默認實現是
ThreadPoolTaskScheduler,它將任務調度到線程池中執行。
開發者也可以自定義 TaskScheduler,用于特定場景(如限制任務并發數量)。
TaskCompletionSource 的作用
在底層,Task 是通過狀態機實現的。狀態機會跟蹤任務的狀態,并在任務完成時觸發相關的回調邏輯。
更多內容參考第八章:封裝與互操作
-
TaskCompletionSource:
TaskCompletionSource是一個用于手動控制Task狀態的工具,開發者可以通過它來完成、取消或設置任務失敗。它常用于將非基于Task的異步操作包裝為Task。示例:
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>(); Task<int> task = tcs.Task; // 在某個異步操作完成后設置結果 tcs.SetResult(42); Console.WriteLine(await task); // 輸出 42
通過 TaskCompletionSource,開發者可以更靈活地控制任務的狀態轉換。
3. Task 的類型
3.1 普通的 Task 和返回值的 Task<T>
Task 有兩種基本類型:
-
Task:
不返回結果的任務,用于執行不需要返回值的異步操作。Task task = Task.Run(() => Console.WriteLine("Hello, Task!")); -
Task<T>:
返回結果的任務,用于執行需要返回值的異步操作。Task<int> task = Task.Run(() => 42); int result = await task; Console.WriteLine(result); // 輸出 42
Task<T> 提供了類型安全的方式來獲取異步操作的結果。
3.2 ValueTask 的引入及其適用場景
為了優化 Task 在高頻調用場景下的性能問題,.NET 引入了 ValueTask,減少任務分配的開銷。
-
ValueTask的特點:- 它可以避免頻繁分配堆內存(
Task通常會在堆上分配)。 - 如果任務已經完成,可以直接返回結果,而無需生成新的任務對象。
- 它可以避免頻繁分配堆內存(
-
適用場景:
- 高頻調用的異步方法。
- 大多數情況下任務已經完成,但仍需要支持異步操作。
示例:
async ValueTask<int> ComputeAsync(bool quick) { if (quick) { return 42; // 同步完成,避免分配額外的 Task 對象 } // 模擬異步完成 return await Task.Delay(1000).ContinueWith(_ => 42); } // 使用 int result = await ComputeAsync(true); -
注意事項:
ValueTask不能多次await或重復使用。- 不適用于所有場景,在復雜任務鏈中仍建議使用
Task。
14.3 async/await 的編譯與運行
1. async/await 是什么?
1.1 async/await 是語法糖
async/await 是一種語法糖,它的作用是讓開發者以同步的方式編寫異步代碼。然而,在運行時,async/await 會被編譯器拆解為一個狀態機,通過狀態機管理異步操作的執行流程。
示例代碼:
public async Task<int> FetchDataAsync()
{
int result = await GetDataAsync();
return result * 2;
}
這段代碼看似同步,但它在編譯后會被重寫為狀態機,異步操作的各個步驟都會被拆解為狀態機的不同狀態,并在狀態之間流轉。
1.2 async/await 的核心:狀態機
async 方法的核心是 編譯器生成的狀態機,它將異步方法拆解為多個狀態,并根據異步操作的完成情況在這些狀態之間切換。
狀態機的職責:
-
保存上下文:
異步方法的局部變量和當前狀態會被保存在狀態機中,以便在異步操作完成后恢復執行。 -
管理狀態流轉:
異步操作完成后,狀態機會根據當前狀態執行相應的邏輯,直到方法執行完畢。 -
掛起和恢復:
當遇到await時,狀態機會掛起當前方法,并在異步任務完成時恢復執行。
2. async/await 的編譯與執行流程
編譯器如何將 async 方法拆解為狀態機?
2.1 異步示例代碼
// 下載器
public class Downloader
{
// 異步方法拉取數據
static async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // 網卡 I/O 異步獲取數據
}
// 處理數據,雖然返回Task<T>,但是是同步方法,只不過方法啟動并返回了一個Task對象,并不執行任何異步操作(await ...)
static Task<string> ProcessDataAsyncAsync(string html)
{
return Task.Run(() =>
{
Thread.Sleep(1000);// 阻塞線程線程池線程1s,模擬 CPU 密集耗時操作
return html.ToUpper(); // 示例處理邏輯
});
}
// 異步保存數據
static async Task SaveDataAsync(string html)
{
await File.WriteAllTextAsync("index.html", html); // 磁盤 I/O 異步保存
}
// 異步從url拉取數據、處理、保存數據
static public async Task SaveDataFromUrlAsync()
{
string url = "https://www.baidu.com";
string html = await FetchDataAsync(url);// 拉取數據
string processedHtml = await ProcessDataAsyncAsync(html);// 處理數據
await SaveDataAsync(processedHtml);// 保存數據
}
}
2.2 ILSpy反編譯源碼
通過ILSpy將編譯后生成的源碼文件反編譯后:
ILSpy反編譯:
得到如圖所示:

完整代碼:
using ...
[NullableContext(1)]
[Nullable(0)]
public class Downloader
{
[CompilerGenerated]
private sealed class <FetchDataAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
[Nullable(0)]
public AsyncTaskMethodBuilder<string> <>t__builder;
[Nullable(0)]
public string url;
[Nullable(0)]
private HttpClient <client>5__1;
[Nullable(0)]
private string <>s__2;
[Nullable(new byte[] { 0, 1 })]
private TaskAwaiter<string> <>u__1;
private void MoveNext()
{
int num = <>1__state;
string result;
try
{
if (num != 0)
{
<client>5__1 = new HttpClient();
}
try
{
TaskAwaiter<string> awaiter;
if (num != 0)
{
awaiter = <client>5__1.GetStringAsync(url).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<FetchDataAsync>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
}
<>s__2 = awaiter.GetResult();
result = <>s__2;
}
finally
{
if (num < 0 && <client>5__1 != null)
{
((IDisposable)<client>5__1).Dispose();
}
}
}
catch (Exception exception)
{
<>1__state = -2;
<client>5__1 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<client>5__1 = null;
<>t__builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[CompilerGenerated]
private sealed class <SaveDataAsync>d__2 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
[Nullable(0)]
public string html;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = File.WriteAllTextAsync("index.html", html).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<SaveDataAsync>d__2 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[CompilerGenerated]
private sealed class <SaveDataFromUrlAsync>d__3 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
[Nullable(0)]
private string <url>5__1;
[Nullable(0)]
private string <html>5__2;
[Nullable(0)]
private string <processedHtml>5__3;
[Nullable(0)]
private string <>s__4;
[Nullable(0)]
private string <>s__5;
[Nullable(new byte[] { 0, 1 })]
private TaskAwaiter<string> <>u__1;
private TaskAwaiter <>u__2;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter<string> awaiter3;
TaskAwaiter<string> awaiter2;
TaskAwaiter awaiter;
switch (num)
{
default:
<url>5__1 = "https://www.baidu.com";
awaiter3 = FetchDataAsync(<url>5__1).GetAwaiter();
if (!awaiter3.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter3;
<SaveDataFromUrlAsync>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
return;
}
goto IL_0090;
case 0:
awaiter3 = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
goto IL_0090;
case 1:
awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
goto IL_010d;
case 2:
{
awaiter = <>u__2;
<>u__2 = default(TaskAwaiter);
num = (<>1__state = -1);
break;
}
IL_010d:
<>s__5 = awaiter2.GetResult();
<processedHtml>5__3 = <>s__5;
<>s__5 = null;
awaiter = SaveDataAsync(<processedHtml>5__3).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 2);
<>u__2 = awaiter;
<SaveDataFromUrlAsync>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
break;
IL_0090:
<>s__4 = awaiter3.GetResult();
<html>5__2 = <>s__4;
<>s__4 = null;
awaiter2 = ProcessDataAsync(<html>5__2).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 1);
<>u__1 = awaiter2;
<SaveDataFromUrlAsync>d__3 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
goto IL_010d;
}
awaiter.GetResult();
}
catch (Exception exception)
{
<>1__state = -2;
<url>5__1 = null;
<html>5__2 = null;
<processedHtml>5__3 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<url>5__1 = null;
<html>5__2 = null;
<processedHtml>5__3 = null;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<FetchDataAsync>d__0))]
[DebuggerStepThrough]
private static Task<string> FetchDataAsync(string url)
{
<FetchDataAsync>d__0 stateMachine = new <FetchDataAsync>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
stateMachine.url = url;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
private static Task<string> ProcessDataAsync(string html)
{
return Task.Run([NullableContext(0)] () =>
{
Thread.Sleep(1000);
return html.ToUpper();
});
}
[AsyncStateMachine(typeof(<SaveDataAsync>d__2))]
[DebuggerStepThrough]
private static Task SaveDataAsync(string html)
{
<SaveDataAsync>d__2 stateMachine = new <SaveDataAsync>d__2();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.html = html;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
[AsyncStateMachine(typeof(<SaveDataFromUrlAsync>d__3))]
[DebuggerStepThrough]
public static Task SaveDataFromUrlAsync()
{
<SaveDataFromUrlAsync>d__3 stateMachine = new <SaveDataFromUrlAsync>d__3();
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
}
2.3 編譯器生成的異步狀態機
完整可執行的狀態機
將2.2去除掉多余的Attribute和一些特殊符號“<”、“>”,美化一下,然后再加上注釋,就成了如下可以直接運行的代碼:
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace SimpleTest;
public class Downloader
{
private sealed class FetchDataAsyncStateMachine : IAsyncStateMachine
{
// 當前狀態(-1: 初始狀態,0: 掛起狀態,-2: 完成狀態)
public int state;
// 異步任務的構建器,用于管理任務的生命周期
public AsyncTaskMethodBuilder<string> taskBuilder;
// 輸入參數:目標 URL
public string url;
// 內部變量:用于 HTTP 請求的 HttpClient
private HttpClient client;
private string fetchedData; // 保存從 URL 獲取的數據
private TaskAwaiter<string> awaiter; // 用于管理 GetStringAsync 的等待狀態
public void MoveNext()
{
int currentState = state; // 保存當前狀態
string result;
try
{
if (currentState != 0) // 狀態為初始狀態
{
client = new HttpClient(); // 創建 HttpClient 實例
}
try
{
TaskAwaiter<string> taskAwaiter;
if (currentState != 0) // 狀態為初始狀態
{
// 開始異步操作,獲取 URL 的內容
taskAwaiter = client.GetStringAsync(url).GetAwaiter();
// 如果異步操作未完成,掛起當前狀態機
if (!taskAwaiter.IsCompleted)
{
state = 0; // 設置狀態為掛起狀態
awaiter = taskAwaiter; // 保存當前的 TaskAwaiter
FetchDataAsyncStateMachine stateMachine = this;
// 將狀態機掛起,等待異步操作完成后繼續
taskBuilder.AwaitUnsafeOnCompleted(ref taskAwaiter, ref stateMachine);
return; // 返回以掛起當前邏輯
}
}
else // 從掛起狀態恢復執行
{
taskAwaiter = awaiter; // 恢復掛起時保存的 TaskAwaiter
awaiter = default; // 清空掛起狀態
state = -1; // 設置狀態為已恢復
}
// 獲取異步操作的結果
fetchedData = taskAwaiter.GetResult();
result = fetchedData;
}
finally
{
// 在操作完成后釋放 HttpClient 資源
if (state < 0 && client != null)
{
client.Dispose();
}
}
}
catch (Exception exception)
{
// 異常處理:設置狀態為完成并報告異常
state = -2;
client = null;
taskBuilder.SetException(exception);
return;
}
// 設置狀態為完成并返回結果
state = -2;
client = null;
taskBuilder.SetResult(result);
}
// 必須實現的接口方法,當前示例中未使用
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
private sealed class SaveDataAsyncStateMachine : IAsyncStateMachine
{
// 當前狀態(-1: 初始狀態,0: 掛起狀態,-2: 完成狀態)
public int state;
// 異步任務的構建器,用于管理任務的生命周期
public AsyncTaskMethodBuilder taskBuilder;
// 輸入參數:要寫入文件的 HTML 數據
public string html;
// 內部變量:管理 WriteAllTextAsync 的等待狀態
private TaskAwaiter awaiter;
public void MoveNext()
{
int currentState = state; // 保存當前狀態
try
{
TaskAwaiter taskAwaiter;
if (currentState != 0) // 初始狀態
{
// 開始異步寫入操作
taskAwaiter = File.WriteAllTextAsync("index.html", html).GetAwaiter();
// 如果寫入操作未完成,掛起當前狀態機
if (!taskAwaiter.IsCompleted)
{
state = 0; // 設置狀態為掛起狀態
awaiter = taskAwaiter; // 保存當前的 TaskAwaiter
SaveDataAsyncStateMachine stateMachine = this;
// 將狀態機掛起,等待寫入操作完成后繼續
taskBuilder.AwaitUnsafeOnCompleted(ref taskAwaiter, ref stateMachine);
return; // 返回以掛起當前邏輯
}
}
else // 從掛起狀態恢復執行
{
taskAwaiter = awaiter; // 恢復掛起時保存的 TaskAwaiter
awaiter = default; // 清空掛起狀態
state = -1; // 設置狀態為已恢復
}
// 獲取異步操作的結果(此處無返回值,單純確保無異常)
taskAwaiter.GetResult();
}
catch (Exception exception)
{
// 異常處理:設置狀態為完成并報告異常
state = -2;
taskBuilder.SetException(exception);
return;
}
// 設置狀態為完成
state = -2;
taskBuilder.SetResult();
}
// 必須實現的接口方法,當前示例中未使用
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
private sealed class SaveDataFromUrlAsyncStateMachine : IAsyncStateMachine
{
// 當前狀態(-1: 初始狀態,0,1,2: 掛起狀態,-2: 完成狀態)
public int state;
public AsyncTaskMethodBuilder taskBuilder;
// 內部變量
private string url; // 請求的 URL
private string html; // 獲取的 HTML 數據
private string processedHtml; // 處理后的 HTML 數據
// 臨時變量
private string tempHtmlResult;
private string tempProcessedResult;
// 用于管理多個異步操作的 Awaiter
private TaskAwaiter<string> awaiter3; // 對應 FetchDataAsync
private TaskAwaiter<string> awaiter2; // 對應 ProcessDataAsync
private TaskAwaiter awaiter; // 對應 SaveDataAsync
public void MoveNext()
{
int currentState = state; // 保存當前狀態機的狀態。初始值為 -1,表示尚未開始執行。
try
{
TaskAwaiter<string> stringTaskAwaiter; // 用于管理異步操作 `FetchDataAsync` 和 `ProcessDataAsync` 的結果。
TaskAwaiter simpleAwaiter; // 用于管理異步操作 `SaveDataAsync` 的結果。
// 根據當前狀態執行不同的邏輯。
switch (currentState)
{
default: // 初始狀態(state = -1)
url = "https://www.baidu.com"; // 初始化 URL 變量,表示目標地址。
// 調用 FetchDataAsync 方法以獲取 HTML 數據,并獲取其 Awaiter。
stringTaskAwaiter = FetchDataAsync(url).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果異步操作尚未完成,則需要掛起狀態機。
{
state = 0; // 將狀態設置為 0,表示掛起點在 FetchDataAsync 處。
awaiter3 = stringTaskAwaiter; // 保存當前的 Awaiter(對應 FetchDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存當前狀態機實例。
// 掛起狀態機,并在異步操作完成后恢復執行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待異步操作完成時重新進入。
}
goto Case_FetchCompleted; // 如果異步操作已完成,直接跳轉到 FetchDataAsync 完成后的邏輯。
case 0: // 從 FetchDataAsync 掛起點恢復
stringTaskAwaiter = awaiter3; // 恢復之前保存的 Awaiter。
awaiter3 = default; // 清除 Awaiter 的引用。
state = -1; // 重置狀態為 -1,表示狀態機當前未掛起。
goto Case_FetchCompleted; // 跳轉到 FetchDataAsync 完成后的邏輯。
case 1: // 從 ProcessDataAsync 掛起點恢復
stringTaskAwaiter = awaiter2; // 恢復之前保存的 Awaiter。
awaiter2 = default; // 清除 Awaiter 的引用。
state = -1; // 重置狀態為 -1,表示狀態機當前未掛起。
goto Case_ProcessCompleted; // 跳轉到 ProcessDataAsync 完成后的邏輯。
case 2: // 從 SaveDataAsync 掛起點恢復
simpleAwaiter = awaiter; // 恢復之前保存的 Awaiter。
awaiter = default; // 清除 Awaiter 的引用。
state = -1; // 重置狀態為 -1,表示狀態機當前未掛起。
break;
Case_FetchCompleted: // FetchDataAsync 操作完成,處理結果
tempHtmlResult = stringTaskAwaiter.GetResult(); // 獲取 FetchDataAsync 的返回結果(HTML 數據)。
html = tempHtmlResult; // 將結果賦值給 html 變量。
tempHtmlResult = null; // 清理臨時變量。
// 調用 ProcessDataAsync 方法以處理 HTML 數據,并獲取其 Awaiter。
stringTaskAwaiter = ProcessDataAsync(html).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果異步操作尚未完成,則需要掛起狀態機。
{
state = 1; // 將狀態設置為 1,表示掛起點在 ProcessDataAsync 處。
awaiter2 = stringTaskAwaiter; // 保存當前的 Awaiter(對應 ProcessDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存當前狀態機實例。
// 掛起狀態機,并在異步操作完成后恢復執行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待異步操作完成時重新進入。
}
goto Case_ProcessCompleted; // 如果異步操作已完成,直接跳轉到 ProcessDataAsync 完成后的邏輯。
Case_ProcessCompleted: // ProcessDataAsync 操作完成,處理結果
tempProcessedResult = stringTaskAwaiter.GetResult(); // 獲取 ProcessDataAsync 的返回結果(處理后的 HTML 數據)。
processedHtml = tempProcessedResult; // 將結果賦值給 processedHtml 變量。
tempProcessedResult = null; // 清理臨時變量。
// 調用 SaveDataAsync 方法以保存處理后的數據,并獲取其 Awaiter。
simpleAwaiter = SaveDataAsync(processedHtml).GetAwaiter();
if (!simpleAwaiter.IsCompleted) // 如果異步操作尚未完成,則需要掛起狀態機。
{
state = 2; // 將狀態設置為 2,表示掛起點在 SaveDataAsync 處。
awaiter = simpleAwaiter; // 保存當前的 Awaiter(對應 SaveDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存當前狀態機實例。
// 掛起狀態機,并在異步操作完成后恢復執行。
taskBuilder.AwaitUnsafeOnCompleted(ref simpleAwaiter, ref stateMachine);
return; // 退出方法,等待異步操作完成時重新進入。
}
break; // 如果異步操作已完成,直接執行 SaveDataAsync 完成后的邏輯。
}
// SaveDataAsync 操作完成,確保任務成功結束
simpleAwaiter.GetResult(); // 調用 GetResult 確保 SaveDataAsync 沒有拋出異常。
}
catch (Exception exception) // 捕獲異步操作中可能拋出的任何異常
{
state = -2; // 將狀態機的狀態設置為 -2,表示已完成且發生異常。
taskBuilder.SetException(exception); // 將捕獲的異常傳遞給 TaskBuilder,通知調用方任務失敗。
return; // 退出方法,狀態機終止。
}
// 異步任務成功完成
state = -2; // 將狀態機的狀態設置為 -2,表示已完成且沒有異常。
taskBuilder.SetResult(); // 標記任務完成并通知調用方。
}
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
private static Task<string> FetchDataAsync(string url) // 異步方法,接收一個 URL 參數,返回一個包含字符串結果的任務。
{
var stateMachine = new FetchDataAsyncStateMachine // 創建 FetchDataAsyncStateMachine 狀態機實例。
{
taskBuilder = AsyncTaskMethodBuilder<string>.Create(), // 初始化 TaskBuilder,用于構建異步任務。
url = url, // 將調用方傳入的 URL 參數賦值到狀態機中。
state = -1 // 初始化狀態為 -1,表示狀態機尚未開始執行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 啟動狀態機,開始執行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 創建的任務,供調用方等待異步操作完成。
}
private static Task<string> ProcessDataAsync(string html) // 異步方法,接收 HTML 字符串,返回處理后的字符串任務。
{
return Task.Run(() => // 使用 Task.Run 在線程池中執行同步代碼,模擬異步操作。
{
Thread.Sleep(1000); // 模擬耗時操作,例如數據處理或計算。
return html.ToUpper(); // 將輸入 HTML 轉換為大寫后返回。
});
}
private static Task SaveDataAsync(string html) // 異步方法,接收 HTML 字符串,返回一個任務。
{
var stateMachine = new SaveDataAsyncStateMachine // 創建 SaveDataAsyncStateMachine 狀態機實例。
{
taskBuilder = AsyncTaskMethodBuilder.Create(), // 初始化 TaskBuilder,用于構建不返回值的異步任務。
html = html, // 將調用方傳入的 HTML 參數賦值到狀態機中。
state = -1 // 初始化狀態為 -1,表示狀態機尚未開始執行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 啟動狀態機,開始執行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 創建的任務,供調用方等待異步操作完成。
}
public static Task SaveDataFromUrlAsync() // 異步方法,無參數,返回一個任務。
{
var stateMachine = new SaveDataFromUrlAsyncStateMachine // 創建 SaveDataFromUrlAsyncStateMachine 狀態機實例。
{
taskBuilder = AsyncTaskMethodBuilder.Create(), // 初始化 TaskBuilder,用于構建不返回值的異步任務。
state = -1 // 初始化狀態為 -1,表示狀態機尚未開始執行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 啟動狀態機,開始執行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 創建的任務,供調用方等待異步操作完成。
}
}
注意:示例使用的是
Debug發布模式,生成的狀態機是一個Class,如果使用的是Release模式發布,生成的狀態機會是一個結構體,以優化性能。
狀態機執行步驟
通過對比異步代碼和編譯器生成的狀態機的代碼,編譯器為每個異步方法生成一個獨立的狀態機類型,這些類實現了 IAsyncStateMachine 接口。
調用并執行一個異步方法實際上就是啟動一個該異步方法對應的狀態機實例。
第一步:啟動狀態機
private static Task<string> FetchDataAsync(string url) // 異步方法,接收一個 URL 參數,返回一個包含字符串結果的任務。
{
var stateMachine = new FetchDataAsyncStateMachine // 創建 FetchDataAsyncStateMachine 狀態機實例。
{
taskBuilder = AsyncTaskMethodBuilder<string>.Create(), // 初始化 TaskBuilder,用于構建異步任務。
url = url, // 將調用方傳入的 URL 參數賦值到狀態機中。
state = -1 // 初始化狀態為 -1,表示狀態機尚未開始執行。
};
stateMachine.taskBuilder.Start(ref stateMachine); // 啟動狀態機,開始執行其 MoveNext 方法。
return stateMachine.taskBuilder.Task; // 返回由 TaskBuilder 創建的任務,供調用方等待異步操作完成。
}
其中taskBuilder.Start(ref stateMachine)源碼如下:
/// <summary>
/// 啟動狀態機的執行。
/// </summary>
/// <typeparam name="TStateMachine">狀態機的類型。</typeparam>
/// <param name="stateMachine">狀態機實例,按引用傳遞。</param>
[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null) // 確保狀態機實例不為 null
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
// 獲取當前線程的執行上下文和同步上下文
Thread currentThread = Thread.CurrentThread;
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext(); // 執行狀態機的下一步邏輯
}
finally
{
// 如果同步上下文發生了變化,恢復為之前的同步上下文
if (previousSyncCtx != currentThread._synchronizationContext)
{
currentThread._synchronizationContext = previousSyncCtx;
}
// 如果執行上下文發生了變化,恢復為之前的執行上下文
if (previousExecutionCtx != currentThread._executionContext)
{
ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentThread._executionContext);
}
}
}
其實就只是開始執行狀態機的MoveNext方法,同時捕獲并在執行后恢復線程的同步上下文和執行上下文,確保上下文一致性不被意外修改。
詳細觀察狀態機的 MoveNext 方法可以發現,異步方法被編譯器通過 await 拆分為多個邏輯塊,每個塊通常對應一個 await 操作(異步操作),比如異步方法 SaveDataFromUrlAsync 的狀態機的MoveNext方法中:
第二步:啟動異步方法FetchDataAsync
當狀態機的狀態是-1時候,也就是初始狀態,MoveNext執行如下內容:
// 根據當前狀態執行不同的邏輯。
switch (currentState)
{
default: // 初始狀態(state = -1)
url = "https://www.baidu.com"; // 初始化 URL 變量,表示目標地址。
// 調用 FetchDataAsync 方法以獲取 HTML 數據,并獲取其 Awaiter。
stringTaskAwaiter = FetchDataAsync(url).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果異步操作尚未完成,則需要掛起狀態機。
{
state = 0; // 將狀態設置為 0,表示掛起點在 FetchDataAsync 處。
awaiter3 = stringTaskAwaiter; // 保存當前的 Awaiter(對應 FetchDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存當前狀態機實例。
// 掛起狀態機,并在異步操作`FetchDataAsync`完成后恢復執行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待異步`FetchDataAsync`操作完成時重新進入。
}
...
}
...
重點解讀:
- 啟動異步方法
FetchDataAsync - 將狀態機的狀態從-1設置成0。
- 然后執行
taskBuilder.AwaitUnsafeOnCompleted - 然后就返回了?嗯?
MoveNext就執行完返回了嗎,那后續狀態怎么執行,在哪里執行。往后看!
第三步:啟動異步方法ProcessDataAsync
當狀態機的狀態是0時候,MoveNext執行如下內容:
case 0: // 從 FetchDataAsync 掛起點恢復
stringTaskAwaiter = awaiter3; // 恢復之前保存的 Awaiter。
awaiter3 = default; // 清除 Awaiter 的引用。
state = -1; // 重置狀態為 -1,表示狀態機當前未掛起。
goto Case_FetchCompleted; // 跳轉到 FetchDataAsync 完成后的邏輯。
...
Case_FetchCompleted: // FetchDataAsync 操作完成,處理結果
tempHtmlResult = stringTaskAwaiter.GetResult(); // 獲取 FetchDataAsync 的返回結果(HTML 數據)。
html = tempHtmlResult; // 將結果賦值給 html 變量。
tempHtmlResult = null; // 清理臨時變量。
// 調用 ProcessDataAsync 方法以處理 HTML 數據,并獲取其 Awaiter。
stringTaskAwaiter = ProcessDataAsync(html).GetAwaiter();
if (!stringTaskAwaiter.IsCompleted) // 如果異步操作尚未完成,則需要掛起狀態機。
{
state = 1; // 將狀態設置為 1,表示掛起點在 ProcessDataAsync 處。
awaiter2 = stringTaskAwaiter; // 保存當前的 Awaiter(對應 ProcessDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存當前狀態機實例。
// 掛起狀態機,并在異步操作完成后恢復執行。
taskBuilder.AwaitUnsafeOnCompleted(ref stringTaskAwaiter, ref stateMachine);
return; // 退出方法,等待異步操作完成時重新進入。
}
重點解讀:
- 先重置一下狀態
- 然后通過
stringTaskAwaiter.GetResult()獲取上一個異步操作的執行結果 - 然后啟動另一個操作
ProcessDataAsync - 將狀態機的狀態從-1設置成1。
- 然后執行
taskBuilder.AwaitUnsafeOnCompleted然后又返回了。
第四步:啟動異步方法SaveDataAsync
當狀態機的狀態是1時候,MoveNext執行如下內容:
case 1: // 從 ProcessDataAsync 掛起點恢復
stringTaskAwaiter = awaiter2; // 恢復之前保存的 Awaiter。
awaiter2 = default; // 清除 Awaiter 的引用。
state = -1; // 重置狀態為 -1,表示狀態機當前未掛起。
goto Case_ProcessCompleted; // 跳轉到 ProcessDataAsync 完成后的邏輯。
...
Case_ProcessCompleted: // ProcessDataAsync 操作完成,處理結果
tempProcessedResult = stringTaskAwaiter.GetResult(); // 獲取 ProcessDataAsync 的返回結果(處理后的 HTML 數據)。
processedHtml = tempProcessedResult; // 將結果賦值給 processedHtml 變量。
tempProcessedResult = null; // 清理臨時變量。
// 調用 SaveDataAsync 方法以保存處理后的數據,并獲取其 Awaiter。
simpleAwaiter = SaveDataAsync(processedHtml).GetAwaiter();
if (!simpleAwaiter.IsCompleted) // 如果異步操作尚未完成,則需要掛起狀態機。
{
state = 2; // 將狀態設置為 2,表示掛起點在 SaveDataAsync 處。
awaiter = simpleAwaiter; // 保存當前的 Awaiter(對應 SaveDataAsync)。
SaveDataFromUrlAsyncStateMachine stateMachine = this; // 保存當前狀態機實例。
// 掛起狀態機,并在異步操作完成后恢復執行。
taskBuilder.AwaitUnsafeOnCompleted(ref simpleAwaiter, ref stateMachine);
return; // 退出方法,等待異步操作完成時重新進入。
}
重點解讀:
- 重置狀態
- 然后通過
stringTaskAwaiter.GetResult()獲取上一個異步操作的執行結果 - 然后啟動最后一個操作
SaveDataAsync - 將狀態機的狀態從-1設置成2。
- 然后執行
taskBuilder.AwaitUnsafeOnCompleted然后又返回了。
第五步:狀態機執行完畢,設置異步方法的最終結果
當狀態機的狀態是2時候,MoveNext執行如下內容:
public void MoveNext()
{
...
try
{
...
switch (currentState)
{
...
case 2: // 從 SaveDataAsync 掛起點恢復
simpleAwaiter = awaiter; // 恢復之前保存的 Awaiter。
awaiter = default; // 清除 Awaiter 的引用。
state = -1; // 重置狀態為 -1,表示狀態機當前未掛起。
break;
...
}
// SaveDataAsync 操作完成,確保任務成功結束
simpleAwaiter.GetResult(); // 調用 GetResult 確保 SaveDataAsync 沒有拋出異常。
}
catch (Exception exception) // 捕獲異步操作中可能拋出的任何異常
{
state = -2; // 將狀態機的狀態設置為 -2,表示已完成且發生異常。
taskBuilder.SetException(exception); // 將捕獲的異常傳遞給 TaskBuilder,通知調用方任務失敗。
return; // 退出方法,狀態機終止。
}
// 異步任務成功完成
state = -2; // 將狀態機的狀態設置為 -2,表示已完成且沒有異常。
taskBuilder.SetResult(); // 標記任務完成并通知調用方。
}
重點解讀:
- 重置狀態,然后
break;,跳出了switch - 將狀態機的狀態設置成-2:
state = -2;表示整個狀態機執行結束。 - 最后設置整個異步任務的結果,由于
SaveDataFromUrlAsync返回的是個Task,任務不包含具體的返回值所以是taskBuilder.SetResult(),否則是taskBuilder.SetResult(result)。
小結
簡單總結一下:
- 狀態機的
MoveNext方法會根據其不同狀態執行不同的代碼段 - 每一段會啟動一個異步操作,對應的就是原異步方法中的
await后面的異步操作。 - state為-1之后的每次
MoveNext調用,都會先獲取前一個異步操作的執行結果。
至此,異步方法對應的整個狀態機的大致執行過程咱們也搞明白了,但還剩一點沒搞明白,就是第一次MoveNext執行后,就就返回了,后續狀態的MoveNext在哪兒執行?什么時候執行?這就要看builder.AwaitUnsafeOnCompleted到底做了什么了,builder.AwaitUnsafeOnCompleted的內部咱們還是一頭霧水,咱們還不知道。
3.AsyncTaskMethodBuilder<T>.AwaitUnsafeOnCompleted源碼解讀
完整源碼
其中AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted涉及到的源碼github鏈接:
AsyncTaskMethodBuilderT.cs:
- AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref Task
? taskField) - GetStateMachineBox
- AwaitUnsafeOnCompleted
(ref TAwaiter awaiter, IAsyncStateMachineBox box)
TaskAwaiter.cs:
Task.cs:
- UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
- AddTaskContinuation(object tc, bool addBeforeOthers)
- AddTaskContinuationComplex(object tc, bool addBeforeOthers)
解讀
讓我們詳細解讀并簡化這段代碼,同時強調關鍵點,以深入理解整個AwaitUnsafeOnCompleted方法的核心邏輯
整體概述
AwaitUnsafeOnCompleted 是異步狀態機的核心部分,它實現了異步方法的掛起和恢復。以下是它的主要職責:
- 捕獲狀態機和上下文:將當前的狀態機(
stateMachine)和上下文(ExecutionContext或同步上下文)綁定到一個包裝器(IAsyncStateMachineBox),以便在異步任務完成后繼續執行。 - 注冊回調:將狀態機的
MoveNext操作注冊為任務完成后的回調。 - 調度執行:根據上下文(比如
SynchronizationContext或TaskScheduler),決定在任務完成后如何恢復回調的執行。 - 處理異常:在掛起和恢復過程中捕獲并處理可能的異常。
分步解析
1. 頂層方法:AwaitUnsafeOnCompleted
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine =>
AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref m_task);
功能:
- 這是一個公開的方法,供編譯器生成的狀態機代碼調用。它的作用是:
- 將
awaiter(表示異步操作的等待器)和stateMachine(當前異步方法的狀態機)傳遞給內部實現。 m_task是當前異步方法的Task,用于跟蹤異步方法的狀態。
- 將
關鍵點:
TAwaiter必須實現ICriticalNotifyCompletion,這是所有awaiter(如TaskAwaiter或ValueTaskAwaiter)的基礎接口。TStateMachine必須實現IAsyncStateMachine,這是所有異步狀態機的基礎接口。
2. 內部實現:AwaitUnsafeOnCompleted
internal static void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter, ref TStateMachine stateMachine, [NotNull] ref Task<TResult>? taskField)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
// 將 stateMachine 包裝成一個 IAsyncStateMachineBox 對象,用于存儲狀態機、任務和上下文信息。
IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine, ref taskField);
// 通過傳遞 awaiter 和 box 連接狀態機與 awaiter,最終將狀態機的 MoveNext 方法注冊為任務完成后的回調。
AwaitUnsafeOnCompleted(ref awaiter, box);
}
功能:
- 這是
AwaitUnsafeOnCompleted的內部實現。它主要完成兩個任務:- 獲取狀態機的包裝器:調用
GetStateMachineBox方法,將狀態機(stateMachine)與當前任務(taskField)綁定到一個IAsyncStateMachineBox對象中。 - 注冊回調:調用另一個重載的
AwaitUnsafeOnCompleted方法,將任務完成后的回調注冊到awaiter。
- 獲取狀態機的包裝器:調用
關鍵點:
IAsyncStateMachineBox是狀態機的包裝器,用于保存上下文和狀態機的執行邏輯(如MoveNext方法)。- 任務(
taskField)和狀態機的綁定使得異步方法的執行狀態可以被跟蹤和恢復。
3. 包裝狀態機:GetStateMachineBox
// 封裝盒是一個特殊的數據結構,用于管理異步方法的執行狀態
private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
ref TStateMachine stateMachine, // 狀態機實例,用于跟蹤異步方法的執行狀態
[NotNull] ref Task<TResult>? taskField) // 可空的 Task<TResult> 引用,用于存儲任務結果
where TStateMachine : IAsyncStateMachine // TStateMachine 必須實現 IAsyncStateMachine 接口
{
// 捕獲當前的 ExecutionContext(執行上下文)
// 用于將當前線程的上下文(如同步上下文、文化信息等)傳遞給異步操作
ExecutionContext? currentContext = ExecutionContext.Capture();
IAsyncStateMachineBox result; // 定義返回值,封裝當前的狀態機實例
// 檢查 taskField 是否已經是一個強類型的 AsyncStateMachineBox<TStateMachine>
if (taskField is AsyncStateMachineBox<TStateMachine> stronglyTypedBox)
{
// 如果封裝盒的上下文與當前上下文不同,則更新上下文
if (stronglyTypedBox.Context != currentContext)
{
stronglyTypedBox.Context = currentContext;
}
// 將封裝盒賦值給 result
result = stronglyTypedBox;
}
else
{
// 如果 taskField 為空或不是正確的封裝盒,則創建一個新的封裝盒
AsyncStateMachineBox<TStateMachine> box = new AsyncStateMachineBox<TStateMachine>();
// 將新的封裝盒賦值給 taskField,這樣外部代碼可以訪問到任務的狀態
taskField = box;
// 初始化封裝盒的狀態機和上下文
box.StateMachine = stateMachine;
box.Context = currentContext;
// 將封裝盒賦值給 result
result = box;
}
// 返回封裝盒
return result;
}
功能:
- 負責將狀態機(
stateMachine)與任務(taskField)綁定到狀態機包裝器(IAsyncStateMachineBox)。 - 捕獲上下文:通過
ExecutionContext.Capture捕獲當前上下文(如同步上下文、線程上下文等),以便任務完成后能正確恢復上下文。
關鍵點:
- 如果任務已經存在且是強類型的是一個強類型的
AsyncStateMachineBox<TStateMachine>,則直接復用;如果任務尚未創建,則創建新的AsyncStateMachineBox。 - 狀態機包裝器的核心是將狀態機的
MoveNext方法與上下文綁定到一起。
4. 注冊回調:AsyncTaskMethodBuilderT的 AwaitUnsafeOnCompleted重載方法
// 該方法負責將狀態機與 Awaiter 關聯起來,并注冊異步操作完成后的回調
internal static void AwaitUnsafeOnCompleted<TAwaiter>(
ref TAwaiter awaiter, // 異步操作的 Awaiter,用于等待異步結果
IAsyncStateMachineBox box) // 狀態機封裝盒,存儲狀態并控制方法的執行流程
where TAwaiter : ICriticalNotifyCompletion // TAwaiter 必須實現 ICriticalNotifyCompletion 接口
{
// 檢查 Awaiter 是否實現了 ITaskAwaiter 接口(通常是標準的 TaskAwaiter)
if (awaiter is ITaskAwaiter taskAwaiter)
{
// 如果是 TaskAwaiter,則調用 UnsafeOnCompletedInternal,關聯任務和狀態機
// continueOnCapturedContext 參數設置為 true,表示繼續在捕獲的上下文中執行后續操作
TaskAwaiter.UnsafeOnCompletedInternal(
taskAwaiter.m_task, // 等待的底層任務
box, // 包裝器,包含狀態機和回調。
continueOnCapturedContext: true // 指定回調(MoveNext)是否在捕獲的同步上下文中恢復
);
}
// 檢查 Awaiter 是否實現了 IConfiguredTaskAwaiter 接口(支持配置的 Awaiter)
else if (awaiter is IConfiguredTaskAwaiter configuredAwaiter)
{
// 如果是 ConfiguredTaskAwaiter,則根據其配置決定上下文捕獲行為
TaskAwaiter.UnsafeOnCompletedInternal(
configuredAwaiter.m_task, // 等待的底層任務
box, // 包裝器,包含狀態機和回調。
(configuredAwaiter.m_options & ConfigureAwaitOptions.ContinueOnCapturedContext) != 0
// 判斷配置是否要求回調(MoveNext)繼續在捕獲的上下文中執行
);
}
else
{
// 對于其他類型的 Awaiter,直接調用其 UnsafeOnCompleted 方法
// 注冊狀態機的 MoveNextAction 作為回調,當異步操作完成時執行
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
}
功能:
- 負責將狀態機的回調(
box.MoveNextAction)注冊到awaiter,以便在任務完成后繼續執行狀態機。 - 根據
awaiter的不同類型,如TaskAwaiter、ConfiguredTaskAwaiter(配置過的Awaiter,比如調用過.ConfigureAwait(false)),選擇不同的方式注冊回調。
關鍵點:
- 如果
awaiter是TaskAwaiter,調用TaskAwaiter.UnsafeOnCompletedInternal。 - 如果是其他類型(如
ValueTaskAwaiter),直接調用UnsafeOnCompleted。
5. 注冊回調:TaskAwaiter的UnsafeOnCompletedInternal
internal static void UnsafeOnCompletedInternal(
Task task, // 等待的任務實例。
IAsyncStateMachineBox stateMachineBox, // 異步狀態機的包裝器,包含狀態機的引用及其回調。
bool continueOnCapturedContext // 是否需要在捕獲的上下文中恢復執行。
)
{
// 檢查是否啟用了調試/事件跟蹤功能
if (TplEventSource.Log.IsEnabled() || Task.s_asyncDebuggingEnabled)
{
// 如果啟用了事件日志或異步調試,調用 SetContinuationForAwait 方法。
// 參數說明:
// - stateMachineBox.MoveNextAction: 狀態機的回調方法(MoveNext)將作為任務完成后的延續。
// - continueOnCapturedContext: 是否需要在捕獲的上下文中恢復執行。
// - flowExecutionContext: false 表示不需要傳遞 ExecutionContext。
task.SetContinuationForAwait(stateMachineBox.MoveNextAction, continueOnCapturedContext, flowExecutionContext: false);
}
else
{
// 如果沒有啟用調試/事件跟蹤,調用 UnsafeSetContinuationForAwait 方法。
// 參數說明:
// - stateMachineBox: 包裝器本身,包含狀態機的引用和回調。
// - continueOnCapturedContext: 是否需要在捕獲的上下文中恢復執行。
task.UnsafeSetContinuationForAwait(stateMachineBox, continueOnCapturedContext);
}
}
功能:
- 將狀態機的回調(
stateMachineBox.MoveNextAction)注冊到任務(task),以便任務完成后調用狀態機的MoveNext方法。 - 如果啟用了調試或事件跟蹤(
TplEventSource.Log.IsEnabled),會記錄調試信息。 - 如果沒有調試需求,則直接調用
UnsafeSetContinuationForAwait。
6. 包裝回調對象并將其添加到任務延續中:Task.UnsafeSetContinuationForAwait
// 用于為當前任務設置異步操作完成后的狀態機回調。
// 根據上下文的捕獲情況決定如何調度回調的執行。
internal void UnsafeSetContinuationForAwait(
IAsyncStateMachineBox stateMachineBox, // 異步狀態機的包裝器,包含狀態機以及完成后的回調。
bool continueOnCapturedContext // 是否需要在捕獲的上下文中恢復執行。
)
{
// 如果需要在捕獲的上下文中恢復執行
if (continueOnCapturedContext)
{
// 檢查當前線程是否有 SynchronizationContext
if (SynchronizationContext.Current is SynchronizationContext syncCtx
&& syncCtx.GetType() != typeof(SynchronizationContext)) // 確保 SynchronizationContext 是自定義實現
{
// 如果存在自定義同步上下文(非默認同步上下文),
// 創建一個針對同步上下文的異步任務延續對象
var tc = new SynchronizationContextAwaitTaskContinuation(
syncCtx, // 當前的 SynchronizationContext,用于調度回調。
stateMachineBox.MoveNextAction, // 狀態機的 MoveNext 方法,作為延續回調。
flowExecutionContext: false // 表示不傳遞 ExecutionContext。
);
// 將任務延續注冊到當前任務中。
// 參數:
// - tc: 延續對象。
// - addBeforeOthers: false,表示將延續添加到任務隊列的末尾。
AddTaskContinuation(tc, addBeforeOthers: false);
}
// 檢查當前是否有自定義 TaskScheduler(并且不是默認的 TaskScheduler)
else if (TaskScheduler.InternalCurrent is TaskScheduler scheduler && scheduler != TaskScheduler.Default)
{
// 如果存在自定義任務調度器,
// 創建一個針對任務調度器的異步任務延續對象
var tc = new TaskSchedulerAwaitTaskContinuation(
scheduler, // 當前的 TaskScheduler,用于調度回調。
stateMachineBox.MoveNextAction, // 狀態機的 MoveNext 方法,作為延續回調。
flowExecutionContext: false // false,表示不傳遞 ExecutionContext。
);
// 將任務延續注冊到當前任務中。
// 參數:
// - tc: 延續對象。
// - addBeforeOthers: false,表示將延續添加到任務隊列的末尾。
AddTaskContinuation(tc, addBeforeOthers: false);
}
// 如果沒有自定義 SynchronizationContext 或 TaskScheduler
else
{
// 直接將狀態機包裝器作為任務的延續。
// 參數:
// - stateMachineBox: 包含任務完成后的回調。
// - addBeforeOthers: false,表示將延續添加到任務隊列的末尾。
AddTaskContinuation(stateMachineBox, addBeforeOthers: false);
}
}
// 如果不需要在捕獲的上下文中恢復執行
else
{
// 直接將狀態機包裝器作為任務的延續。
AddTaskContinuation(stateMachineBox, addBeforeOthers: false);
}
}
功能:
- 檢查當前的上下文(
SynchronizationContext或TaskScheduler),決定在任務完成后如何調度狀態機的執行。 - 需要上下文切換:
- 如果存在
SynchronizationContext且不是默認實現,則將當前同步上下文和回調(MoveNextAction)包裝成一個同步上下文任務延續(SynchronizationContextAwaitTaskContinuation),然后添加到任務的延續(回調)中。 - 如果存在自定義的
TaskScheduler,則將當前調度器和回調(MoveNextAction)包裝成一個調度器任務延續(TaskSchedulerAwaitTaskContinuation),然后添加到任務的延續(回調)中。
- 如果存在
- 無需上下文切換:
- 如果沒有上下文要求,則直接將狀態機的包裝器(
IAsyncStateMachineBox)作為回調,并通過AddTaskContinuation注冊。
- 如果沒有上下文要求,則直接將狀態機的包裝器(
上下文調度機制:
- 如果使用
SynchronizationContextAwaitTaskContinuation,最終會調用syncCtx.Post(MoveNextAction),以確保回調在捕獲的上下文中執行。 - 如果使用
TaskSchedulerAwaitTaskContinuation,回調將通過指定的任務調度器調度。
詳細了解調度,請參考:第十三章:調度
7. 注冊回調到任務中:AddTaskContinuation
private bool AddTaskContinuation(object tc, bool addBeforeOthers)
{
Debug.Assert(tc != null);
// 如果任務已經完成,則無法繼續添加回調,直接返回 false
if (IsCompleted) return false;
// 嘗試將回調對象直接存儲到 m_continuationObject 字段中
if ((m_continuationObject != null) || (Interlocked.CompareExchange(ref m_continuationObject, tc, null) != null))
{
// 如果 m_continuationObject 已經有值,則進入復雜邏輯
return AddTaskContinuationComplex(tc, addBeforeOthers);
}
else
{
// 如果成功將回調存儲到 m_continuationObject 中,返回 true
return true;
}
}
功能:
-
檢查任務完成狀態:
- 如果任務已經完成,則無法繼續添加回調,直接返回
false,表示添加失敗。
- 如果任務已經完成,則無法繼續添加回調,直接返回
-
快速存儲單個回調:
- 如果當前
m_continuationObject為空,則通過Interlocked.CompareExchange嘗試將回調對象tc原子性地存儲到m_continuationObject中。 - 如果存儲成功,表示此任務僅有一個回調,返回
true。
- 如果當前
-
多回調處理:
- 如果
m_continuationObject已經存在值,則調用AddTaskContinuationComplex,將回調列表化并處理多回調的邏輯。
- 如果
8. 多回調處理:AddTaskContinuationComplex
private bool AddTaskContinuationComplex(object tc, bool addBeforeOthers)
{
Debug.Assert(tc != null, "Expected non-null tc object in AddTaskContinuationComplex");
object? oldValue = m_continuationObject;
Debug.Assert(oldValue is not null, "Expected non-null m_continuationObject object");
// 如果任務已經完成,則無法添加回調,直接返回 false
if (oldValue == s_taskCompletionSentinel)
{
return false;
}
// 如果當前只存儲了單個回調對象,則將其轉換為回調列表
List<object?>? list = oldValue as List<object?>;
if (list is null)
{
// 構造一個新的回調列表,將舊回調和新的回調一起存儲
list = new List<object?>();
if (addBeforeOthers)
{
list.Add(tc);
list.Add(oldValue);
}
else
{
list.Add(oldValue);
list.Add(tc);
}
// 嘗試將回調列表存儲到 m_continuationObject 中
object? expected = oldValue;
oldValue = Interlocked.CompareExchange(ref m_continuationObject, list, expected);
if (oldValue == expected)
{
// 如果存儲成功,返回 true
return true;
}
// 如果存儲失敗,重新檢查 m_continuationObject 的狀態
list = oldValue as List<object?>;
if (list is null)
{
Debug.Assert(oldValue == s_taskCompletionSentinel, "Expected m_continuationObject to be list or sentinel");
return false;
}
}
// 如果 m_continuationObject 已經是一個回調列表,則直接將新的回調添加到列表中
lock (list)
{
// 如果任務已經完成,則無法添加回調,返回 false
if (m_continuationObject == s_taskCompletionSentinel)
{
return false;
}
// 清理列表中的空條目(可能是由于移除操作導致的)
if (list.Count == list.Capacity)
{
list.RemoveAll(l => l == null);
}
// 根據 `addBeforeOthers` 參數決定將新的回調添加到列表的頭部或尾部
if (addBeforeOthers)
{
list.Insert(0, tc);
}
else
{
list.Add(tc);
}
}
return true; // 回調成功添加到列表中
}
功能:
-
檢查任務完成狀態:
- 如果
m_continuationObject是s_taskCompletionSentinel(表示任務已完成),則無法添加新的回調。
- 如果
-
將單回調轉換為列表:
- 如果當前
m_continuationObject只存儲了單個回調,則將其轉換為回調列表,并將新回調一同存儲。
- 如果當前
-
多回調列表的處理:
- 如果
m_continuationObject已經是一個回調列表,則直接將新回調添加到列表中。 - 根據
addBeforeOthers參數,決定將新的回調添加到頭部或尾部。
- 如果
-
線程安全:
- 使用
Interlocked.CompareExchange和lock確保多線程環境下的操作安全。
- 使用
總結與重點
-
核心目標:
AwaitUnsafeOnCompleted的核心目標是將狀態機的MoveNext方法注冊為任務完成后的回調,并根據捕獲的上下文/調度器及要求決定回調的執行是否需要調度到對應的上下文/調度器。- 通過
UnsafeSetContinuationForAwait,異步狀態機能夠在任務完成后恢復執行,且始終在正確的上下文中運行。
-
狀態機包裝器:
- 使用
GetStateMachineBox創建或獲取IAsyncStateMachineBox,將狀態機與任務綁定。 - 包裝器會捕獲狀態機的
MoveNext方法和當前執行上下文。
- 使用
-
回調注冊:
UnsafeSetContinuationForAwait調用AddTaskContinuation,將狀態機的回調注冊到任務。- 如果任務已經完成,回調會立即觸發;否則,等待任務完成后觸發。
-
上下文切換:
- 根據
SynchronizationContext或TaskScheduler決定回調的執行是否需要切換上下文。 - 如果需要上下文切換,會通過上下文調度器(如
SynchronizationContext.Post或TaskScheduler.QueueTask)確保回調在正確的環境中執行。
- 根據
-
調試支持:
- 在調試模式下,通過
TplEventSource.Log添加事件跟蹤和任務活動記錄。
- 在調試模式下,通過
4. await 的核心
await 的本質是對一個實現了 Awaiter 模式 的對象進行操作。Awaiter 模式由以下方法和屬性組成:
-
GetAwaiter方法:
返回一個Awaiter對象,該對象必須實現以下方法和屬性。 -
IsCompleted屬性:
表示異步操作是否已經完成。 -
OnCompleted方法:
注冊一個回調,當異步操作完成時調用。 -
GetResult方法:
獲取異步操作的結果。如果任務失敗,會拋出異常。
3.1 Awaiter 模式的實現
以下是一個簡化的 Awaiter 示例:
public class MyAwaiter : INotifyCompletion
{
private Task _task;
public MyAwaiter(Task task)
{
_task = task;
}
public bool IsCompleted => _task.IsCompleted;
public void OnCompleted(Action continuation)
{
_task.ContinueWith(_ => continuation());
}
public void GetResult()
{
_task.Wait(); // 等待任務完成
}
}
當我們調用 await 時,編譯器會將代碼拆解為類似以下形式:
var awaiter = myAwaitable.GetAwaiter(); // 獲取 Awaiter
if (!awaiter.IsCompleted) // 如果任務尚未完成
{
awaiter.OnCompleted(() => MoveNext()); // 注冊回調
return; // 暫停當前方法
}
awaiter.GetResult(); // 獲取任務結果或拋出異常
3.2 SynchronizationContext 的作用
在使用 await 時,默認情況下,恢復操作會在捕獲的上下文中執行,例如:
-
UI 應用(WPF/WinForms):
恢復操作會切換回主線程,以便更新 UI。 -
ASP.NET Core:
默認沒有捕獲上下文,恢復操作直接在線程池中執行。
SynchronizationContext 是負責上下文切換的核心組件。await 會調用 SynchronizationContext.Post 方法,將后續代碼調度到適當的線程。
3.3 為什么 ConfigureAwait(false) 可以禁用上下文捕獲?
默認情況下,await 會捕獲當前的 SynchronizationContext,以便在異步操作完成后切換回原來的上下文。但在某些場景(例如后臺服務或性能敏感的代碼中),這種切換可能是多余的。
通過調用 ConfigureAwait(false),可以禁用上下文捕獲,直接在線程池中執行后續代碼:
await SomeAsyncMethod().ConfigureAwait(false);
具體原因參考4. 注冊回調:AsyncTaskMethodBuilderT的 AwaitUnsafeOnCompleted
這樣可以避免上下文切換帶來的性能開銷,但需要注意禁用上下文捕獲后,后續操作在線程池線程上執行,因此無法直接更新 UI。
14.4 自定義Task、async/await實現
上一節通過在源碼層面深入解讀了async/await的原理,以及 Awaiter 模式,本節通過自定義 Task 和Awaiter 模式來加深對async/await的理解。
自定義Task:CustomTask
using System;
using System.Threading;
namespace SimpleTest;
// 自定義Task
public class CustomTask<T>
{
private T _result; // 保存任務結果
private Exception? _exception; // 保存任務中的異常
private bool _isCompleted; // 是否已完成
// 將 _continuation 的訪問修飾符改為 internal
internal Action? _continuation; // 異步完成后的回調
private readonly object _lock = new(); // 用于線程安全的鎖
// 構造函數:接受一個工作委托并啟動
public CustomTask(Func<T> work)
{
if (work == null) throw new ArgumentNullException(nameof(work)); // 確保工作委托不為 null
// 啟動一個線程來執行任務
new Thread(() =>
{
try
{
_result = work(); // 執行任務并保存結果
}
catch (Exception ex)
{
_exception = ex; // 捕獲異常
}
finally
{
SetCompleted(); // 標記任務完成
}
}).Start();
}
// 啟動一個自定義Task
public static CustomTask<T> Run(Func<T> work)
{
return new CustomTask<T>(work);
}
// 設置任務為完成狀態,并觸發回調
private void SetCompleted()
{
lock (_lock)
{
_isCompleted = true; // 標記任務已完成
_continuation?.Invoke(); // 如果有回調,立即調用
}
}
// 獲取自定義 Awaiter 實例
public CustomAwaiter<T> GetAwaiter()
{
return new CustomAwaiter<T>(this, continueOnCapturedContext: true);
}
// 提供 ConfigureAwait 功能
public ConfiguredCustomAwaiter ConfigureAwait(bool continueOnCapturedContext)
{
return new ConfiguredCustomAwaiter(this, continueOnCapturedContext);
}
// 任務狀態屬性
public bool IsCompleted
{
get
{
lock (_lock) return _isCompleted;
}
}
// 獲取任務結果
public T Result
{
get
{
lock (_lock)
{
if (!_isCompleted)
{
throw new InvalidOperationException("任務尚未完成,無法獲取結果。");
}
if (_exception != null)
{
throw _exception; // 如果任務失敗,拋出異常
}
return _result; // 返回結果
}
}
}
// 內部結構體:用于支持 ConfigureAwait 功能
public readonly struct ConfiguredCustomAwaiter
{
private readonly CustomTask<T> _task;
private readonly bool _continueOnCapturedContext;
public ConfiguredCustomAwaiter(CustomTask<T> task, bool continueOnCapturedContext)
{
_task = task;
_continueOnCapturedContext = continueOnCapturedContext;
}
public CustomAwaiter<T> GetAwaiter()
{
return new CustomAwaiter<T>(_task, _continueOnCapturedContext);
}
}
}
自定義Awaiter:CustomAwaiter
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace SimpleTest;
// 自定義 Awaiter
public class CustomAwaiter<T> : INotifyCompletion
{
private readonly CustomTask<T> _task; // 任務實例
private readonly bool _continueOnCapturedContext; // 是否繼續捕獲同步上下文
public CustomAwaiter(CustomTask<T> task, bool continueOnCapturedContext)
{
_task = task ?? throw new ArgumentNullException(nameof(task)); // 確保任務不為 null
_continueOnCapturedContext = continueOnCapturedContext; // 保存上下文捕獲設置
}
// 屬性:任務是否完成
public bool IsCompleted => _task.IsCompleted;
// 方法:注冊異步完成后的回調
public void OnCompleted(Action continuation)
{
if (continuation == null) throw new ArgumentNullException(nameof(continuation)); // 確保回調不為 null
if (_task.IsCompleted)
{
continuation(); // 如果任務已完成,直接調用回調
}
else
{
// 保存回調,在任務完成時調用
lock (_task)
{
if (_task.IsCompleted)
{
continuation(); // 避免競爭條件,檢查狀態后再調用
}
else
{
_task._continuation += () =>
{
if (_continueOnCapturedContext)
{
var syncContext = SynchronizationContext.Current; // 獲取當前的同步上下文
if (syncContext != null)
{
syncContext.Post(_ => continuation(), null); // 在同步上下文中調度回調
return;
}
}
continuation(); // 不捕獲上下文時,直接調用回調
};
}
}
}
}
// 方法:獲取任務結果
public T GetResult()
{
return _task.Result; // 返回任務結果
}
}
使用自定義的Task和Awaiter
static async void TestCustomTaskAsync()
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 任務開始...");
// 使用 CustomTask.Run 啟動一個任務
var customTask = CustomTask<int>.Run(() =>
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 任務執行中...");
Thread.Sleep(2000); // 模擬耗時操作
return 42; // 返回計算結果
});
// 使用 ConfigureAwait(false) 防止捕獲同步上下文
int result = await customTask.ConfigureAwait(false);
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 任務完成,結果:{result}");
}
TestCustomTaskAsync();
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] 主線程繼續執行...");
Thread.Sleep(3000); // 確保異步任務完成
14.5 async/await 為什么這么好用?
async/await 是 .NET 中異步編程的核心,它提供了一種簡潔、直觀的方式來編寫異步代碼,同時解決了傳統異步編程中的許多痛點。本節將從以下幾個方面探討 async/await 的優勢。
1. 同步方式編寫異步代碼
1.1 如何避免回調地獄?
在傳統異步模型中,回調函數(callback)是異步操作的主要手段。例如,JavaScript 中的異步操作曾經大量依賴回調函數。這種方式雖然有效,但容易導致“回調地獄”(callback hell),使代碼變得難以閱讀和維護:
// 傳統回調式代碼(偽代碼)
DoSomethingAsync(result1 =>
{
DoSomethingElseAsync(result1, result2 =>
{
FinalStepAsync(result2, finalResult =>
{
Console.WriteLine(finalResult);
});
});
});
使用 async/await 后,異步代碼可以像同步代碼一樣按順序書寫,大大提升了代碼的可讀性和可維護性:
// 使用 async/await
var result1 = await DoSomethingAsync();
var result2 = await DoSomethingElseAsync(result1);
var finalResult = await FinalStepAsync(result2);
Console.WriteLine(finalResult);
優勢:
- 消除了嵌套: 每一行代碼表示一個明確的操作,邏輯清晰,不再需要過多的閉包和嵌套結構。
- 更易維護: 當業務邏輯改變時,只需調整對應的邏輯代碼,而不用修改復雜的回調鏈。
- 更貼近同步邏輯: 異步代碼的執行方式接近于同步代碼,開發者無需掌握復雜的異步編程模型。
但目前 async/await 的根本原理還是基于回調,但已經被編譯器隱藏起來了。
1.2 代碼的可讀性和可維護性如何得到提升?
-
從事件驅動到線性邏輯:
async/await將事件驅動的異步編程簡化為線性順序的代碼邏輯,開發者無需顯式管理回調函數。 -
狀態管理自動化:
使用傳統異步方式時,開發者需要顯式保存和恢復狀態(例如通過閉包)。而async/await自動生成狀態機,幫助保存方法的執行上下文和異步操作的執行狀態。 -
異常處理統一:
異步代碼的異常可以通過try/catch捕獲,不需要顯式處理每個回調的錯誤,減少了大量的錯誤處理代碼。
示例:傳統異步 vs async/await
傳統異步代碼:
DoSomethingAsync(
success =>
{
if (success)
{
DoSomethingElseAsync(
result =>
{
if (result == null)
{
HandleError("Result is null");
return;
}
Console.WriteLine("Success!");
},
error =>
{
HandleError(error);
});
}
else
{
HandleError("DoSomethingAsync failed");
}
},
error =>
{
HandleError(error);
});
使用 async/await 后:
try
{
bool success = await DoSomethingAsync();
if (!success) throw new Exception("DoSomethingAsync failed");
var result = await DoSomethingElseAsync();
if (result == null) throw new Exception("Result is null");
Console.WriteLine("Success!");
}
catch (Exception ex)
{
HandleError(ex.Message);
}
總結:
async/await 提供了接近同步代碼的寫法,減少了嵌套、狀態管理和顯式回調,使代碼更易于閱讀和維護。
2. 錯誤處理
2.1 異常傳播機制:為什么可以用 try/catch 捕獲異步代碼中的異常?
在 async/await 的實現中,異常傳播遵循以下規則:
- 當異步方法中拋出異常時,異常會被捕獲并存儲在返回的
Task對象中。 - 如果在調用異步方法時使用了
await,異常會在await的位置重新拋出。 - 異常傳播的機制使得我們可以像同步代碼一樣使用
try/catch捕獲異步方法中的異常。
示例:
async Task<int> DivideAsync(int a, int b)
{
if (b == 0) throw new DivideByZeroException("Cannot divide by zero");
return a / b;
}
async Task TestAsync()
{
try
{
int result = await DivideAsync(10, 0);
Console.WriteLine($"Result: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
}
輸出:
Caught exception: Cannot divide by zero
2.2 Task 的 AggregateException 與 await 異常傳播的關系
如果直接訪問 Task 的結果(例如通過 Task.Result 或 Task.Wait),異常會被包裝在 AggregateException 中:
try
{
var task = DivideAsync(10, 0);
task.Wait(); // 或 task.Result
}
catch (AggregateException aggEx)
{
foreach (var ex in aggEx.InnerExceptions)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
}
但是,如果使用 await,異常會自動從 AggregateException 中提取出來并直接拋出,更符合開發者的期望:
try
{
int result = await DivideAsync(10, 0);
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception: {ex.Message}");
}
總結:
async/await 自動提取異常,避免了開發者處理 AggregateException 的復雜性,使異常處理更加直觀。
3. 性能優化
3.1 為什么 async/await 的性能比傳統異步模型更優?
傳統異步模型(如基于線程的異步編程)常常需要顯式創建和管理線程、上下文切換以及狀態保存,這些操作開銷較大。而 async/await 基于以下優化實現高效的異步操作:
-
基于狀態機的輕量開銷:
async/await編譯器會為每個異步方法生成一個隱式的狀態機,負責管理方法的執行狀態。- 狀態機的創建和調度開銷遠低于顯式線程的創建和切換。
-
任務復用:
- 異步方法只有在真正遇到異步操作(例如
await)時,才會掛起并返回控制權。未使用await的異步方法會直接同步執行,避免不必要的性能開銷。
- 異步方法只有在真正遇到異步操作(例如
-
線程池的高效利用:
async/await不會阻塞線程。方法掛起時,線程可以被釋放,用于處理其他任務。- 對于 I/O 操作,
async/await會使用操作系統底層的異步 API,避免線程的占用。
3.2 基于狀態機的輕量開銷
編譯器會將每個 async 方法轉換為一個隱式的狀態機。這個狀態機負責:
- 保存異步方法的執行狀態(例如當前執行到哪一步)。
- 在異步操作完成后,恢復執行。
示例:簡單的 async 方法狀態機
async Task<int> SampleAsync()
{
await Task.Delay(1000);
return 42;
}
編譯器生成的狀態機偽代碼:
struct SampleAsyncStateMachine : IAsyncStateMachine
{
public int State; // 保存當前狀態
public AsyncTaskMethodBuilder<int> Builder; // 構建器
public void MoveNext()
{
switch (State)
{
case 0:
Task.Delay(1000).ContinueWith(()=>MoveNext());
State = 1;
return;
case 1:
Builder.SetResult(42);
return;
}
}
}
3.3 異步方法的延遲執行與線程池的高效利用
async/await 的設計避免了不必要的線程創建:
- 異步方法只有在遇到
await時才會掛起。如果異步方法沒有await,它會像普通方法一樣同步執行。 - 掛起后,線程會被釋放,避免線程阻塞,提高線程池的利用率。
總結
async/await 的強大之處在于:
- 同步方式編寫異步代碼: 簡化了異步編程邏輯,消除了回調地獄。
- 錯誤處理: 提供了清晰的異常傳播機制,支持統一的
try/catch異常處理。 - 性能優化: 基于狀態機的輕量實現,結合線程池和延遲執行,提供了高效的異步性能。
這些特性使得 async/await 成為 .NET 中異步編程的首選工具,大幅提升了開發效率和代碼質量。
14.6 async/await 與同步上下文的協作
SynchronizationContext 負責在異步操作完成后,決定代碼的執行上下文(例如線程、特定的調度環境)。本節將詳細講解 SynchronizationContext 的作用、await 的線程切換行為,以及 ConfigureAwait(false) 的使用。
1. SynchronizationContext 的作用
1.1 什么是 SynchronizationContext?
SynchronizationContext 是 .NET 中的一個抽象類,表示一個同步上下文。它在異步編程中用于協調一些操作的執行位置,例如:
- 在 UI 應用程序中,確保異步操作完成后回到主線程更新 UI。
- 在 ASP.NET 中,確保異步操作完成后代碼繼續在請求上下文中運行。
每種應用程序框架都會實現自己的 SynchronizationContext:
- WinForms 和 WPF: 使用
WindowsFormsSynchronizationContext確保異步操作完成后回到主線程操作 UI。 - ASP.NET: 為每個請求創建一個
AspNetSynchronizationContext,確保異步操作完成后繼續在該請求上下文中執行。 - ASP.NET Core: 默認沒有
SynchronizationContext,改用線程池執行異步操作。
1.2 如何在 UI 應用中切換到主線程?
在 UI 應用程序(如 WinForms 和 WPF)中,主線程負責管理用戶界面。因此,當異步操作完成后,我們需要切換回主線程來更新 UI。這是通過 SynchronizationContext 實現的。
示例:WPF 應用中使用主線程更新 UI
private async void Button_Click(object sender, RoutedEventArgs e)
{
// 異步操作(默認捕獲主線程 SynchronizationContext)
await Task.Delay(2000);
// 回到主線程,更新 UI
MyLabel.Content = "操作完成!";
}
在上面的代碼中:
- 默認情況下,在
await之后,代碼通過捕獲的SynchronizationContext調度,最終在主線程中執行。 - 這使得我們可以安全地更新 UI,而無需顯式調用
Dispatcher.Invoke。
1.3 ASP.NET Core 默認不帶 SynchronizationContext 的原因及優勢
在 ASP.NET Core 中,默認的 SynchronizationContext 被移除,異步操作完成后直接在線程池中運行。這與傳統的 ASP.NET 不同。
原因:
- 性能提升: 移除
SynchronizationContext消除了請求上下文的綁定,減少了線程切換的開銷。 - 無需線程綁定: ASP.NET Core 的設計目標是高性能和高并發,它允許異步操作在不同線程中完成,而不必回到特定線程或上下文。
優勢:
- 異步操作的線程切換更少,提高了服務器的吞吐量。
- 開發者無需擔心線程上下文綁定,代碼可以更加自由地運行在線程池線程上。
示例:ASP.NET Core 中的異步方法
public async Task<IActionResult> GetDataAsync()
{
var data = await SomeAsyncOperation();
return Ok(data); // 無需返回到請求上下文
}
在 ASP.NET Core 中,await 后的代碼會直接在線程池線程中運行,而不會嘗試恢復到原始的請求上下文。
2. await 的線程切換行為
2.1 await 前后的線程是否一致?
await 的線程切換行為取決于是否捕獲了 SynchronizationContext 或當前執行的線程上下文:
- 如果捕獲了
SynchronizationContext(例如在 WPF 或 WinForms 中),await后的代碼會切回到捕獲的線程(通常是主線程)。 - 如果沒有
SynchronizationContext(例如在控制臺應用程序或 ASP.NET Core 中),await后的代碼會繼續在線程池中的線程運行。
示例:不同場景下 await 的線程切換
private async Task TestAsync()
{
Console.WriteLine($"Before await: Thread {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000); // 模擬異步操作
Console.WriteLine($"After await: Thread {Thread.CurrentThread.ManagedThreadId}");
}
運行結果:
-
在 WPF 或 WinForms 中:
Before await: Thread 1 After await: Thread 1(因為捕獲了主線程的
SynchronizationContext) -
在控制臺應用程序或 ASP.NET Core 中:
Before await: Thread 1 After await: Thread 4(因為沒有
SynchronizationContext,await后的代碼可能運行在不同的線程上)
2.2 ConfigureAwait(false) 的適用場景及性能提升
ConfigureAwait 是一個重要的工具,用于控制 await 是否捕獲當前的 SynchronizationContext。
ConfigureAwait(true)(默認): 捕獲當前的SynchronizationContext,await后的代碼會切換回原始上下文。ConfigureAwait(false): 不捕獲SynchronizationContext,await后的代碼在線程池中運行。
適用場景
-
ConfigureAwait(true):- 在需要返回到特定上下文的場景中使用,例如:
- WPF 或 WinForms 應用程序中更新 UI。
- ASP.NET 中恢復到請求上下文。
- 在需要返回到特定上下文的場景中使用,例如:
-
ConfigureAwait(false):- 在不依賴特定上下文的后臺操作中使用,例如:
- 數據庫查詢、文件讀寫、網絡請求等不涉及 UI 的操作。
- 提升性能,減少線程切換開銷。
- 在不依賴特定上下文的后臺操作中使用,例如:
性能提升
ConfigureAwait(false)避免了捕獲和恢復SynchronizationContext的開銷,尤其是在高并發場景下。- 在 ASP.NET Core 中,由于沒有
SynchronizationContext,默認行為類似于ConfigureAwait(false)。
示例:使用 ConfigureAwait(false) 提升性能
public async Task ProcessDataAsync()
{
// 不捕獲同步上下文,提高性能
var data = await GetDataFromDatabaseAsync().ConfigureAwait(false);
// 繼續處理數據(運行在線程池線程上)
ProcessData(data);
}
14.7 async/await 與執行上下文的協作
在 .NET 中,執行上下文(ExecutionContext) 在異步代碼和多線程編程中用于管理與邏輯操作相關的上下文信息。async/await 是 .NET 異步編程的核心特性,與執行上下文密切協作,以確保異步操作中的上下文一致性。本節將詳細探討執行上下文的原理、作用,以及它與 async/await 的協作方式。
1. 執行上下文(ExecutionContext)
1.1 什么是執行上下文
ExecutionContext 是 .NET 中的一個類,表示代碼執行時的邏輯上下文。它封裝了與當前線程和任務相關的一些信息,確保這些信息在異步方法或線程切換中能夠正確傳遞。
執行上下文中包含的信息:
- 同步上下文(SynchronizationContext): 決定異步任務完成后代碼的調度位置(如主線程)。
- 安全上下文(SecurityContext): 包含與線程安全相關的信息,例如用戶身份驗證和權限。
- 邏輯調用上下文(Logical Call Context): 支持跨線程傳遞的上下文數據,例如通過
CallContext或AsyncLocal存儲的值。
執行上下文的核心作用是 在異步操作和線程切換中傳遞這些關鍵信息,以確保邏輯行為的一致性。
1.2 執行上下文的作用
執行上下文在以下方面具有重要作用:
-
上下文一致性:
- 在異步操作或線程切換中,執行上下文會捕獲并恢復相關信息,使得上下文數據在整個異步調用鏈中保持一致。
- 例如,
HttpContext和AsyncLocal的值會通過執行上下文正確傳遞。
-
跨線程邏輯數據管理:
AsyncLocal和CallContext依賴執行上下文來在異步方法間傳遞數據。
-
安全性:
- 執行上下文可以攜帶用戶身份、權限等信息,在異步方法中保證安全上下文的一致性。
-
調試和診斷:
- 執行上下文中的信息(例如請求 ID、日志上下文)可以用于跨線程/異步操作的調試和日志記錄。
1.3 執行上下文的傳遞
執行上下文的傳遞是通過 捕獲(Capture) 和 恢復(Restore) 實現的:
-
捕獲:
- 當創建異步任務或啟動新線程時,.NET 會捕獲當前線程的執行上下文并與異步任務或線程關聯。
- 捕獲的內容包括同步上下文、AsyncLocal 值、權限信息等。
-
恢復:
- 當異步任務完成并切換回主上下文時,.NET 會恢復捕獲的執行上下文,以確保上下文數據一致。
示例:執行上下文傳遞
AsyncLocal會隨著執行上下文進行傳遞,因此示例使用AsyncLocal
static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
_asyncLocal.Value = "Main Context";
Console.WriteLine($"Before Task: {_asyncLocal.Value}");
await Task.Run(() =>
{
Console.WriteLine($"Inside Task Before: {_asyncLocal.Value}");
_asyncLocal.Value = "Task Context";
Console.WriteLine($"Inside Task After: {_asyncLocal.Value}");
});
Console.WriteLine($"After Task: {_asyncLocal.Value}");
}
輸出結果:
Before Task: Main Context
Inside Task Before: Main Context
Inside Task After: Task Context
After Task: Main Context
分析:
- 在
Task.Run中,執行上下文的值從主上下文傳遞給了異步任務。 - 異步任務修改了上下文值,但這種修改僅在異步任務范圍內生效。
- 當返回到主上下文時,
_asyncLocal.Value恢復為主上下文的值。
1.4 執行上下文與性能優化
捕獲和恢復執行上下文是有開銷的,尤其是在高并發或性能敏感的場景中。為了減少開銷,可以采取以下優化措施:
-
禁止執行上下文的流動(SuppressFlow):
- 使用
ExecutionContext.SuppressFlow()方法可以禁用執行上下文的捕獲和恢復。 - 適用于不需要跨異步任務傳遞上下文數據的場景(例如純計算任務)。
示例:禁用執行上下文流動
ExecutionContext.SuppressFlow(); await Task.Run(() => { // 這里不會捕獲和傳遞主上下文的信息 Console.WriteLine(_asyncLocal.Value); // 輸出為空 }); ExecutionContext.RestoreFlow(); - 使用
-
顯式傳遞上下文數據:
- 如果上下文數據可以通過參數傳遞,則避免使用
AsyncLocal或CallContext。
- 如果上下文數據可以通過參數傳遞,則避免使用
-
限制上下文捕獲:
- 在 ASP.NET Core 中,通過
ConfigureAwait(false)可以避免捕獲同步上下文,從而提升性能。
- 在 ASP.NET Core 中,通過
2. async/await 與執行上下文的協作
async/await 是如何做到執行上下文的一致性的呢?
2.1 await 前后如何保證執行上下文的一致性
在 async 方法中,await 會捕獲當前線程的執行上下文ExecutionContext,并在異步操作完成后恢復。
示例:async/await 的上下文捕獲
static AsyncLocal<string> _asyncLocal = new AsyncLocal<string>();
static async Task Main(string[] args)
{
_asyncLocal.Value = "Main Context";
Console.WriteLine($"Before await: {_asyncLocal.Value}");
await Task.Delay(100); // 異步操作,可能切換線程
Console.WriteLine($"After await: {_asyncLocal.Value}");
}
輸出結果:
Before await: Main Context
After await: Main Context
分析:
- 在
await之前,主上下文中的_asyncLocal.Value被捕獲。 - 異步任務完成后,捕獲的上下文被恢復,因此上下文值保持一致。
原理:
前面我們已經知道,await后面的操作會被包裝成異步任務回調執行,因此要確保await前后執行上下文一致性,就需要將執行上下文一起包裝到回調中,這樣回調執行時,就可以恢復其上下文。那是如何、何時將執行上下文傳遞到回調中的呢,看如下代碼:
包裝狀態機:GetStateMachineBox
// 封裝盒是一個特殊的數據結構,用于管理異步方法的執行狀態
private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
ref TStateMachine stateMachine, // 狀態機實例,用于跟蹤異步方法的執行狀態
[NotNull] ref Task<TResult>? taskField) // 可空的 Task<TResult> 引用,用于存儲任務結果
where TStateMachine : IAsyncStateMachine // TStateMachine 必須實現 IAsyncStateMachine 接口
{
// 捕獲當前的 ExecutionContext(執行上下文)
// 用于將當前線程的上下文(如同步上下文、文化信息等)傳遞給異步操作
ExecutionContext? currentContext = ExecutionContext.Capture();
IAsyncStateMachineBox result; // 定義返回值,封裝當前的狀態機實例
// 檢查 taskField 是否已經是一個強類型的 AsyncStateMachineBox<TStateMachine>
if (taskField is AsyncStateMachineBox<TStateMachine> stronglyTypedBox)
{
// 如果封裝盒的上下文與當前上下文不同,則更新上下文
if (stronglyTypedBox.Context != currentContext)
{
stronglyTypedBox.Context = currentContext;
}
// 將封裝盒賦值給 result
result = stronglyTypedBox;
}
else
{
// 如果 taskField 為空或不是正確的封裝盒,則創建一個新的封裝盒
AsyncStateMachineBox<TStateMachine> box = new AsyncStateMachineBox<TStateMachine>();
// 將新的封裝盒賦值給 taskField,這樣外部代碼可以訪問到任務的狀態
taskField = box;
// 初始化封裝盒的狀態機和上下文
box.StateMachine = stateMachine;
box.Context = currentContext;
// 將封裝盒賦值給 result
result = box;
}
// 返回封裝盒
return result;
}
解讀:
其實也就是在包裝回調狀態機的時候將執行上下文一起打包了。然后異步任務完成后執行回調時,判斷有沒有特定的執行上下文,有就恢復。
2.2 線程如何確保調用異步任務后的執行上下文不被改變?
前面我們已經知道,啟動一個異步操作,實際就是啟動這個異步操作對應的狀態機,執行其MoveNext方法。
啟動代碼如下:
/// <summary>
/// 啟動狀態機的執行。
/// </summary>
/// <typeparam name="TStateMachine">狀態機的類型。</typeparam>
/// <param name="stateMachine">狀態機實例,按引用傳遞。</param>
[DebuggerStepThrough]
public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
if (stateMachine == null) // 確保狀態機實例不為 null
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
}
// 獲取當前線程的執行上下文和同步上下文
Thread currentThread = Thread.CurrentThread;
ExecutionContext? previousExecutionCtx = currentThread._executionContext;
SynchronizationContext? previousSyncCtx = currentThread._synchronizationContext;
try
{
stateMachine.MoveNext(); // 啟動狀態機
}
finally
{
// 如果同步上下文發生了變化,恢復為之前的同步上下文
if (previousSyncCtx != currentThread._synchronizationContext)
{
currentThread._synchronizationContext = previousSyncCtx;
}
// 如果執行上下文發生了變化,恢復為之前的執行上下文
if (previousExecutionCtx != currentThread._executionContext)
{
ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentThread._executionContext);
}
}
}
解讀:
- 可以看到,Start方法在啟動異步操作前會先獲取當前的執行上下文
ExecutionContext,然后在啟動異步操作后恢復執行上下文ExecutionContext - 同時,同步上下文也會被捕獲,啟動狀態機后被恢復。
14.8 AsyncLocal 與異步編程中的數據流轉
- AsyncLocal 的定義:
- 如何實現異步任務中的上下文數據流轉?
- 與 ThreadLocal 的對比。
- 性能與最佳實踐:
- AsyncLocal 的開銷是否可控?
- 適用于哪些場景?
14.9 async/await 與 Task 的常見誤區
async/await 和 Task 是 .NET 中異步編程的核心工具,但它們的工作機制容易被誤解,導致許多開發者在編寫異步代碼時犯下常見錯誤。本節將澄清一些常見的誤區,幫助開發者更好地理解和使用這些工具。
1. async 不等于多線程
1.1 為什么異步方法并不一定會創建線程?
一個常見的誤解是,async 方法會自動創建新的線程。但實際上,async 和線程沒有直接關系。async/await 關注的是 非阻塞,而不是并行計算。
- 異步方法的主要目的是釋放當前線程,在等待操作完成時讓線程可以執行其他任務,而不是專門創建新線程。
- 異步操作(例如網絡 I/O、文件 I/O)通常由操作系統或硬件處理,不需要線程的參與。只有當異步操作完成時,任務調度器才會安排一個線程來繼續執行后續代碼。
示例:異步方法不創建線程
static async Task Main(string[] args)
{
Console.WriteLine($"Start: Thread {Thread.CurrentThread.ManagedThreadId}");
// 異步等待 I/O 操作,線程未阻塞
await Task.Delay(1000);
Console.WriteLine($"End: Thread {Thread.CurrentThread.ManagedThreadId}");
}
輸出:
Start: Thread 1
End: Thread 1
分析:
Task.Delay模擬異步 I/O 操作,不占用任何線程。- 異步任務完成后,代碼繼續在原線程上執行。
1.2 async/await 關注的是非阻塞,而不是并行
async/await 的設計目標是優化程序的可伸縮性,而非提高并行度。通過釋放線程資源,async/await 可以讓線程池中的線程處理更多任務,從而提升系統吞吐量。
如果需要真正的并行計算(例如 CPU 密集型任務),可以使用 Task.Run 將任務分配到線程池線程,但這不是 async/await 的核心功能。
誤區:async 方法自動并行執行
async Task<int> ComputeAsync()
{
// 不會自動并行,每個方法仍然是順序執行
return await Task.FromResult(42);
}
async Task MainAsync()
{
var result1 = await ComputeAsync();
var result2 = await ComputeAsync();
Console.WriteLine(result1 + result2); // 順序執行
}
正確理解:
- 異步方法的執行順序與普通方法相同,
async只是通過await暫停方法的執行,讓線程可以去處理其他任務。
2. await 的行為
2.1 await 會阻塞線程嗎?
await 不會阻塞線程。它的作用是“暫時掛起”代碼的執行,釋放當前線程以便執行其他任務。當異步操作完成后,await 會通過回調將掛起的代碼恢復執行。
示例:await 不阻塞線程
static async Task Main(string[] args)
{
Console.WriteLine("Before await");
// 模擬異步 I/O 操作
await Task.Delay(1000);
Console.WriteLine("After await");
}
輸出:
Before await
After await
分析:
- 在
Task.Delay的等待期間,當前線程被釋放,主線程沒有被阻塞。
2.2 await 后的代碼總是在當前線程上運行嗎?
await 后的代碼是否運行在當前線程上,取決于是否有 同步上下文(SynchronizationContext)。
- 在 WPF 或 WinForms 等 UI 應用中,
SynchronizationContext會強制await后的代碼返回到 UI 線程。 - 在 ASP.NET Core 或控制臺應用中,默認沒有
SynchronizationContext,await后的代碼可能運行在不同的線程上。
示例:await 后的線程可能不同
static async Task Main(string[] args)
{
Console.WriteLine($"Thread before await: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"Thread after await: {Thread.CurrentThread.ManagedThreadId}");
}
輸出(控制臺應用中):
Thread before await: 1
Thread after await: 4
分析:
- 在控制臺應用中,
await后的代碼可能運行在不同的線程上(通常是線程池線程)。 - 如果需要優化性能,可以通過
ConfigureAwait(false)避免捕獲上下文。
3. Task.Run 的錯誤使用
3.1 為什么不應該濫用 Task.Run?
Task.Run 是一個用于將任務分配到線程池線程的方法,但濫用它會導致以下問題:
-
不必要的線程切換:
- 如果任務本身是異步的(如 I/O 操作),不需要額外使用線程池線程。
-
線程池資源浪費:
- 線程池的線程資源有限,濫用
Task.Run會導致線程池飽和,影響其他任務的執行。
- 線程池的線程資源有限,濫用
錯誤示例:濫用 Task.Run 包裝異步方法
// 錯誤:Task.Run 包裝異步方法,額外增加線程切換
await Task.Run(async () =>
{
await Task.Delay(1000); // 異步操作不需要線程池線程
});
正確做法:直接調用異步方法
// 正確:直接使用異步方法,無需額外創建線程
await Task.Delay(1000);
3.2 CPU 密集型任務與 I/O 密集型任務的正確處理方式
-
CPU 密集型任務:
- 使用
Task.Run將任務分配到線程池線程,避免阻塞主線程。 - 示例:圖像處理、加密運算等。
// 使用 Task.Run 處理 CPU 密集型任務 var result = await Task.Run(() => { return Compute(); }); - 使用
-
I/O 密集型任務:
- 直接使用異步方法(如
await文件 I/O 或網絡請求),不需要Task.Run。 - 示例:讀取文件、調用網絡 API。
// 使用 I/O 異步方法 var data = await File.ReadAllTextAsync("data.txt"); - 直接使用異步方法(如
4. 并發與并行
4.1 async/await 是否等于并發?
async/await 本身并不代表并發。await 會暫停方法的執行,釋放線程資源,但代碼仍然是順序執行的。
- 如果需要并發執行多個任務,需要顯式啟動多個任務(例如使用
Task.WhenAll或Task.WhenAny)。
4.2 如何通過 Task.WhenAll 實現真正的并發?
通過 Task.WhenAll 可以同時啟動多個任務并等待它們全部完成,以實現真正的并發。
示例:并發執行多個任務
async Task MainAsync()
{
// 啟動多個任務
var task1 = Task.Delay(1000);
var task2 = Task.Delay(2000);
var task3 = Task.Delay(3000);
// 等待所有任務完成
await Task.WhenAll(task1, task2, task3);
Console.WriteLine("All tasks completed");
}
輸出:
任務并發執行,總耗時約為 3 秒。
14.10 async/await 的性能優化與高級用法
async/await 提供了一種簡單高效的異步編程模型,但在高性能應用中,默認的 Task 和狀態機生成可能會引入額外的開銷。本節從性能優化和高級用法兩個方面,探討如何進一步提升 async/await 的性能,以及如何定制異步行為。
1. 性能優化
1.1 使用 ValueTask 優化高頻異步方法的性能
在某些高頻調用的場景中,返回 Task 會引入額外的內存分配,因為每次調用都會創建一個新的 Task 對象。對于快速完成的異步方法,這種內存分配是沒有必要的。在這些場景下,可以使用 ValueTask 來優化性能。
- 什么是
ValueTask?
ValueTask是 .NET 提供的一種輕量級異步返回類型,用于避免不必要的任務分配。如果一個異步方法經常同步完成,ValueTask可以直接返回結果,而無需創建額外的Task對象。
示例:使用 ValueTask 優化簡單異步方法
參考 第三章
1.2 避免不必要的上下文捕獲 (ConfigureAwait(false) 的使用場景)
await 默認會捕獲當前的上下文(SynchronizationContext 或 TaskScheduler),并在異步操作完成后恢復到該上下文。這種上下文捕獲在大多數場景下是必要的(如 UI 應用程序),但在后臺任務或性能敏感的場景中,這可能會增加不必要的開銷。
-
性能問題:
上下文捕獲和恢復會導致額外的線程切換,這在高并發場景中是昂貴的。 -
解決方案:
使用ConfigureAwait(false)避免捕獲上下文,從而減少開銷。
示例:使用 ConfigureAwait(false) 優化性能
async Task PerformBackgroundTaskAsync()
{
// 模擬異步操作
await Task.Delay(100).ConfigureAwait(false);
// 此處代碼不需要回到原上下文
Console.WriteLine("Task completed on thread pool");
}
使用場景:
- 在后臺服務(如 ASP.NET Core)中,異步方法通常不需要捕獲上下文。
- 在性能敏感的場景中,盡量避免不必要的上下文切換。
1.3 減少狀態機的開銷(async 方法何時不生成狀態機?)
async 方法會被編譯器轉換成狀態機,以支持掛起和恢復操作。但如果方法不包含 await 或可以同步完成,編譯器會優化,避免生成狀態機。
示例:不生成狀態機的場景(正確示范)
// 不包含 await,不會生成狀態機
Task<int> GetValueAsync()
{
return Task.FromResult(42); // 直接返回結果
}
或者
// 不包含 await,不會生成狀態機
Task<int> GetValueAsync()
{
return Task.Run(...); // 直接返回Task
}
示例:生成狀態機的場景(不建議示范)
// 不包含 await,不會生成狀態機
async Task<int> GetValueAsync()
{
return await Task.FromResult(42); // 直接返回結果
}
或者
// 不包含 await,不會生成狀態機
async Task<int> GetValueAsync()
{
return await Task.Run(...); // 直接返回Task
}
注意:
- 如果方法包含
await,狀態機是不可避免的。 - 對于簡單的同步返回場景,避免使用
async修飾符是更高效的選擇。
2. 高級用法
2.1 如何為自定義類型添加 await 支持?
2.3 使用 TaskCompletionSource 手動控制異步任務
TaskCompletionSource 是一個強大的工具,用于手動控制異步任務的完成狀態。它通常用于橋接同步代碼和異步代碼之間的調用。
- 常見場景:
- 封裝回調形式的異步操作。
- 提供更精細的異步任務控制。
示例:使用 TaskCompletionSource 封裝異步回調
public static Task<int> GetResultAsync()
{
var tcs = new TaskCompletionSource<int>();
// 模擬異步回調
Task.Run(() =>
{
Thread.Sleep(1000); // 模擬耗時操作
tcs.SetResult(42); // 手動設置任務結果
});
return tcs.Task;
}
static async Task Main(string[] args)
{
int result = await GetResultAsync();
Console.WriteLine($"Result: {result}");
}
輸出:
Result: 42
注意:
TaskCompletionSource提供了SetResult、SetException和SetCanceled方法,用于手動控制任務狀態。- 使用時需要注意線程安全。

浙公網安備 33010602011771號