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

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