1. 異步編程的基礎(chǔ)
1.1 什么是異步編程?
異步編程是一種編程范式,旨在解決傳統(tǒng)同步編程中因等待操作(如I/O或計算)而導(dǎo)致的線程阻塞問題。在同步模型中,調(diào)用一個耗時操作會使當前線程暫停,直到操作完成。而在異步模型中,程序可以在等待操作完成的同時繼續(xù)執(zhí)行其他任務(wù),從而提高資源利用率和程序的響應(yīng)性。
例如,在處理網(wǎng)絡(luò)請求時,同步調(diào)用會阻塞線程直到響應(yīng)返回,而異步調(diào)用則允許線程去做其他工作,待響應(yīng)到達時再處理結(jié)果。這種特性在I/O密集型場景(如文件讀寫、網(wǎng)絡(luò)通信)和高并發(fā)場景(如Web服務(wù)器)中尤為重要。
1.2 C#中的async
和await
C#通過async
和await
關(guān)鍵字簡化了異步編程的編寫:
- **
async
**:標記一個方法為異步方法,表示它可能包含異步操作。通常與Task
或Task<T>
返回類型一起使用。 - **
await
**:暫停異步方法的執(zhí)行,等待某個異步操作(通常是Task
)完成,同時釋放當前線程。
以下是一個簡單的異步方法示例:
public async Task<int> GetNumberAsync()
{
await Task.Delay(1000); // 模擬1秒延遲
return 42;
}
調(diào)用此方法時,await Task.Delay(1000)
會暫停方法執(zhí)行,但不會阻塞線程。線程會被釋放,待延遲完成后,方法繼續(xù)執(zhí)行并返回結(jié)果。
2. 編譯器的魔力:狀態(tài)機
2.1 異步方法的轉(zhuǎn)換
盡管async
和await
讓異步代碼看起來像同步代碼,但這背后是C#編譯器的復(fù)雜工作。當您編寫一個async
方法時,編譯器會將其轉(zhuǎn)換為一個狀態(tài)機(State Machine),負責(zé)管理異步操作的執(zhí)行流程。
狀態(tài)機是一個自動機,它將方法的執(zhí)行分解為多個狀態(tài),每個狀態(tài)對應(yīng)代碼中的一個執(zhí)行階段(通常是await
點)。狀態(tài)機通過暫停和恢復(fù)機制,確保方法能在異步操作完成時正確繼續(xù)執(zhí)行。
2.2 狀態(tài)機的結(jié)構(gòu)
編譯器生成的的狀態(tài)機通常是一個結(jié)構(gòu)體(在發(fā)布模式下以減少分配開銷)或類(在調(diào)試模式下以便調(diào)試),實現(xiàn)了IAsyncStateMachine
接口。該接口定義了兩個方法:
- **
MoveNext
**:驅(qū)動狀態(tài)機執(zhí)行,是狀態(tài)機的核心邏輯。 - **
SetStateMachine
**:用于跨AppDomain場景,通常不直接使用。
狀態(tài)機包含以下關(guān)鍵字段:
- **
state
**:一個整數(shù),表示當前狀態(tài)(如-1表示初始,0、1等表示等待點,-2表示完成)。 - **
builder
**:AsyncTaskMethodBuilder
或AsyncTaskMethodBuilder<T>
,用于構(gòu)建和完成返回的Task
。 - **
awaiter
**:表示當前等待的異步操作(如TaskAwaiter
)。
2.3 狀態(tài)機的執(zhí)行流程
以GetNumberAsync
為例,其狀態(tài)機的執(zhí)行流程如下:
- 初始狀態(tài)(state = -1):方法開始執(zhí)行。
- **遇到
await
**:檢查Task.Delay(1000)
是否已完成。- 如果未完成,狀態(tài)機將:
- 更新
state
為0(表示等待第一個await
)。 - 注冊一個延續(xù)(continuation),等待任務(wù)完成時回調(diào)。
- 返回,釋放線程。
- 如果已完成,直接繼續(xù)執(zhí)行。
- 任務(wù)完成:任務(wù)完成時觸發(fā)延續(xù),狀態(tài)機恢復(fù):
- 檢查
state
值為0,跳轉(zhuǎn)到await
后的代碼。 - 獲取結(jié)果,繼續(xù)執(zhí)行。
- 方法完成(state = -2):設(shè)置返回值并完成
Task
。
以下是簡化的狀態(tài)機偽代碼:
private struct GetNumberAsyncStateMachine : IAsyncStateMachine
{
public int state; // 狀態(tài)字段
public AsyncTaskMethodBuilder<int> builder; // Task構(gòu)建器
private TaskAwaiter awaiter; // 等待器
public void MoveNext()
{
int result;
try
{
if (state == -1) // 初始狀態(tài)
{
awaiter = Task.Delay(1000).GetAwaiter();
if (!awaiter.IsCompleted) // 任務(wù)未完成
{
state = 0; // 等待狀態(tài)
builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); // 注冊延續(xù)
return;
}
goto resume0; // 已完成,直接繼續(xù)
}
if (state == 0) // 從await恢復(fù)
{
resume0:
awaiter.GetResult(); // 獲取結(jié)果
result = 42;
builder.SetResult(result); // 設(shè)置返回值
state = -2; // 完成
}
}
catch (Exception ex)
{
builder.SetException(ex); // 設(shè)置異常
state = -2;
}
}
}
2.4 狀態(tài)機圖示
為了更直觀地理解,我們將從宏觀角度理解狀態(tài)機(State Machine)的組件及其交互邏輯,以下是一個狀態(tài)機流程圖:
3. 任務(wù)(Task)的奧秘
3.1 Task的定義
Task
是C#異步編程的核心類,位于System.Threading.Tasks
命名空間。它表示一個異步操作,可以是計算任務(wù)、I/O操作或任何異步工作。Task<T>
是帶返回值的版本。
3.2 Task的生命周期
Task
有以下狀態(tài)(通過Task.Status
屬性查看):
- Created:已創(chuàng)建但未調(diào)度。
- WaitingToRun:已調(diào)度但等待執(zhí)行。
- Running:正在執(zhí)行。
- RanToCompletion:成功完成。
- Faulted:發(fā)生異常。
- Canceled:被取消。
3.3 Task的調(diào)度
Task
的執(zhí)行由任務(wù)調(diào)度器(TaskScheduler)管理。默認調(diào)度器使用線程池(ThreadPool)來執(zhí)行任務(wù)。線程池是一個預(yù)分配的線程集合,可以重用線程,避免頻繁創(chuàng)建和銷毀線程的開銷。
創(chuàng)建Task
的方式包括:
- **
Task.Run
**:將任務(wù)調(diào)度到線程池執(zhí)行。 - **
Task.Factory.StartNew
**:更靈活的創(chuàng)建方式。 - 異步方法返回的Task:由
AsyncTaskMethodBuilder
管理。
3.4 I/O-bound vs CPU-bound任務(wù)
- I/O-bound任務(wù):如網(wǎng)絡(luò)請求(
HttpClient.GetAsync
)、文件操作(File.ReadAllTextAsync
),使用異步I/O機制,通常不占用線程,而是通過操作系統(tǒng)提供的回調(diào)完成。 - CPU-bound任務(wù):如復(fù)雜計算(
Task.Run(() => Compute())
),在線程池線程上執(zhí)行。
例如:
public async Task<string> FetchDataAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://example.com"); // I/O-bound
}
public Task<int> ComputeAsync()
{
return Task.Run(() => { /* CPU密集型計算 */ return 42; }); // CPU-bound
}
4. 線程管理和上下文
異步編程的核心目標是避免線程阻塞,而不是頻繁切換線程。想象一個應(yīng)用程序,比如一個帶有用戶界面的程序,主線程(通常是UI線程)負責(zé)處理用戶交互、繪制界面等任務(wù)。如果某個操作(比如網(wǎng)絡(luò)請求或文件讀寫)需要很長時間,主線程如果傻等,就會導(dǎo)致程序卡頓。異步編程通過將耗時任務(wù)“卸載”出去,讓主線程繼續(xù)執(zhí)行其他工作,從而保持程序的響應(yīng)性。
在C#中,async
和await
關(guān)鍵字極大簡化了異步編程,但其底層依賴于狀態(tài)機和任務(wù)調(diào)度。
異步并不總是意味著線程切換,而是通過合理的任務(wù)分配和通知機制實現(xiàn)非阻塞。
4.1 線程切換是如何發(fā)生的?
異步操作中是否涉及線程切換,取決于任務(wù)的類型和執(zhí)行環(huán)境。我們可以把任務(wù)分為兩類:
I/O密集型任務(wù)(I/O-bound)
- 比如網(wǎng)絡(luò)請求、文件讀寫等,這些任務(wù)通常由系統(tǒng)內(nèi)核或線程池線程在后臺處理。
- 主線程發(fā)起請求后,立即返回,不會被阻塞。當任務(wù)完成時,系統(tǒng)通過回調(diào)或延續(xù)(continuation)通知主線程。
- 例子:你調(diào)用
HttpClient.GetAsync()
,主線程發(fā)起請求后繼續(xù)執(zhí)行,網(wǎng)絡(luò)操作由底層線程池或系統(tǒng)完成,結(jié)果回來時觸發(fā)延續(xù)。
CPU密集型任務(wù)(CPU-bound)
- 比如復(fù)雜的數(shù)學(xué)計算,這種任務(wù)可以交給線程池線程執(zhí)行,避免阻塞主線程。
- 例子:用
Task.Run()
將計算任務(wù)交給線程池,主線程繼續(xù)處理其他邏輯。
需要注意的是,在某些情況下,異步操作可能根本不涉及線程切換。例如,一個同步完成的I/O操作(比如從緩存讀取數(shù)據(jù))或使用Task.Yield()
,都可能在同一線程上完成。
4.2 C#中async/await的工作原理
在C#中,當你使用async
和await
時,編譯器會將方法轉(zhuǎn)化為一個狀態(tài)機。這個狀態(tài)機負責(zé):
- 在
await
處暫停方法的執(zhí)行。 - 設(shè)置一個延續(xù)(continuation),表示任務(wù)完成后要繼續(xù)執(zhí)行的代碼。
- 當任務(wù)完成時,觸發(fā)狀態(tài)機恢復(fù)執(zhí)行,從
await
后的代碼繼續(xù)。
關(guān)鍵機制:
- 同步上下文(SynchronizationContext):在UI應(yīng)用中,
await
會捕獲當前的同步上下文(通常是UI線程上下文),確保任務(wù)完成后的延續(xù)回到UI線程執(zhí)行,以便更新界面。 ConfigureAwait(false)
:如果不需要回到原線程(比如在服務(wù)器端代碼中),可以用這個選項讓延續(xù)在線程池線程上執(zhí)行,減少線程切換開銷。
4.3 線程切換的開銷
線程切換涉及上下文切換(保存和恢復(fù)線程狀態(tài)),開銷不小。因此,異步編程的目標是減少不必要的切換。比如:
- 在UI應(yīng)用中,延續(xù)默認回到UI線程,確保界面更新安全。
- 在服務(wù)器端,
ConfigureAwait(false)
可以避免切換回原上下文,提升性能。
異步編程通過將耗時任務(wù)委托給后臺線程或系統(tǒng)內(nèi)核,避免主線程阻塞,而不是依賴頻繁的線程切換。你的比喻基本合理,尤其是“主線程交給另一輛車”的想法,但需要強調(diào)主線程不等待、結(jié)果通過信號通知的特點。改進后的比喻更準確地反映了異步的非阻塞特性和線程管理機制。
4.4 幾個重要概念
4.4.1 同步上下文(SynchronizationContext)
同步上下文是一個抽象類,用于在特定線程或上下文中執(zhí)行代碼。在UI應(yīng)用程序(如WPF、WinForms)中,UI線程有一個特定的SynchronizationContext
,確保UI更新在UI線程上執(zhí)行。
await
默認會捕獲當前的同步上下文,并在任務(wù)完成后恢復(fù)到該上下文執(zhí)行后續(xù)代碼。例如:
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(1000);
label.Text = "Done"; // 自動恢復(fù)到UI線程
}
4.4.2 ConfigureAwait 的作用
ConfigureAwait(bool continueOnCapturedContext)
允許控制是否恢復(fù)到原始上下文:
- **
true
**(默認):恢復(fù)到捕獲的上下文。 - **
false
**:在任務(wù)完成后的任意線程上繼續(xù)執(zhí)行。
在服務(wù)器端代碼中,使用ConfigureAwait(false)
可以避免不必要的上下文切換:
public async Task<string> GetDataAsync()
{
await Task.Delay(1000).ConfigureAwait(false);
return "Data"; // 不恢復(fù)到原始上下文
}
即使有人對async/await
的工作流程有了相當不錯的理解,但對于嵌套異步調(diào)用鏈的行為仍有很多困惑。尤其是討論到在庫代碼中何時以及如何使用ConfigureAwait(false)
時,這種困惑更為明顯。接下來我們通過下面的流程圖,探索一個非常具體的示例,并深入理解每一個執(zhí)行步驟:

https://vkontech.com/exploring-the-async-await-state-machine-series-overview/
4.4.3 執(zhí)行上下文(ExecutionContext)
執(zhí)行上下文維護線程的執(zhí)行環(huán)境,包括安全上下文、調(diào)用上下文等。在異步操作中,ExecutionContext
會被捕獲并在延續(xù)時恢復(fù),確保線程局部數(shù)據(jù)(如ThreadLocal<T>
)的正確性。
5. 異常處理機制
5.1 異常的捕獲和傳播
在異步方法中,拋出的異常會被捕獲并存儲在返回的Task
中。當await
該Task
時,異常會被重新拋出。例如:
public async Task ThrowAsync()
{
await Task.Delay(1000);
throw new Exception("Error");
}
public async Task CallAsync()
{
try
{
await ThrowAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // 輸出 "Error"
}
}
5.2 狀態(tài)機中的異常處理
狀態(tài)機的MoveNext
方法包含try-catch塊,捕獲異常并通過builder.SetException
設(shè)置到Task
中,如前述偽代碼所示。
5.3 聚合異常
如果一個Task
等待多個子任務(wù)(如Task.WhenAll
),可能會拋出AggregateException
,包含所有子任務(wù)的異常。await
會自動解包,拋出第一個異常。
6. 自定義Awaiter和擴展性
6.1 Awaiter模式
C#支持await任何實現(xiàn)了awaiter模式的類型,要求:
- 提供
GetAwaiter
方法,返回一個awaiter對象。 - awaiter實現(xiàn)
INotifyCompletion
(或ICriticalNotifyCompletion
),并提供:bool IsCompleted
:指示任務(wù)是否完成。GetResult
:獲取結(jié)果或拋出異常。
6.2 自定義Awaiter的用途
例如,ValueTask<T>
是一個輕量級替代Task<T>
的結(jié)構(gòu),用于高頻調(diào)用場景減少內(nèi)存分配:
public ValueTask<int> ComputeValueAsync()
{
return new ValueTask<int>(42); // 同步完成,無需分配Task
}
7. 實際應(yīng)用與示例分析
7.1 異步方法的編寫
編寫異步方法的最佳實踐:
- 使用
async Task
或async Task<T>
作為返回類型。 - 避免
async void
,除非是事件處理程序。 - 在非UI代碼中使用
ConfigureAwait(false)
。
7.2 異步流(C# 8.0+)
異步流(IAsyncEnumerable<T>
)允許異步生成和消費數(shù)據(jù)序列:
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100);
yield return i;
}
}
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number);
}
8. 總結(jié)與實踐建議
C#的異步編程通過async
和await
,結(jié)合狀態(tài)機、任務(wù)調(diào)度和線程管理,實現(xiàn)了高效的非阻塞代碼。其底層原理包括:
- 狀態(tài)機:編譯器將異步方法轉(zhuǎn)換為狀態(tài)機,管理暫停和恢復(fù)。
- Task:表示異步操作,由任務(wù)調(diào)度器和線程池執(zhí)行。
- 上下文:同步上下文和執(zhí)行上下文確保線程安全性。
- 異常處理:異常在Task中傳播,await時重新拋出。
實踐建議:
- 使用
ConfigureAwait(false)
優(yōu)化服務(wù)器端性能。 - 確保異常在合適的地方被捕獲和處理。
- 將CPU-bound任務(wù)調(diào)度到線程池,避免阻塞UI線程。
- 利用異步流處理大數(shù)據(jù)或?qū)崟r數(shù)據(jù)。
通過理解這些底層機制,有助于我們更高效地編寫異步代碼,從而構(gòu)建高性能、可伸縮的應(yīng)用程序。
?轉(zhuǎn)自https://www.cnblogs.com/code-daily/p/18909634
該文章在 2025/6/11 9:36:30 編輯過