.NET 11 的 Runtime Async

 整理一下 .NET 11 Preview 1 的新功能:Runtime Async。



聲明:本文提到的 .NET 11 runtime async 功能,係針對 2026 年 2 月發布的 .NET 11 預覽版 Preview 1。未來 .NET 11 正式版發布時可能還有變動。


.NET 11 Preview 1 的發行說明 中提到了一個新功能:runtime async。有了這項功能,.NET 11 的 runtime 會把 async 方法視為一等公民,親自接管非同步方法的執行與狀態管理。


這表示長久以來,C# 編譯器在背後替 async 方法添加狀態機的機制即將走入歷史。對開發人員來說,則是立刻享有效能提升的好處。至少有以下幾點:

  • 減少 IL 程式碼與提升 JIT 執行效率: C# 編譯器不會再替 async 方法產生一堆處理狀態機的 IL code,所以編譯出來的 .NET 執行檔/DLL 檔案會更小,而 JIT 的即時編譯速度也會更快、所需的記憶體更少。
  • 減少記憶體分配: 傳統上,C# 編譯器產生的狀態機結構會被裝箱(boxed)到 heap 上,而 .NET 11 由 runtime 負責管理 async 方法執行流程之後,能利用速度更快的 stack 記憶體區塊來處理相關變數,從而大幅減少裝箱到 heap 的情形,並減輕 Garbage Collector (GC) 的壓力。
  • 除錯體驗可望更為直觀: 以往在除錯非同步程式碼時,call stack(呼叫堆疊)常會充斥著編譯器合成的 MoveNext 方法(類似 <GetData>d__0.MoveNext())。未來由 runtime 接管後,call stack 會變得更乾淨,stack trace 也(應該)會更接近原始程式碼的樣貌。
  • 非同步呼叫的最佳化: .NET runtime 接管非同步方法的呼叫之後,將能夠對連續的方法呼叫鏈(chains of async calls)進行最佳化。

如何啟用 runtime async

我用的是 Visual Studio 2026 Insiders 的 Community 版本,搭配 .NET 11.0 SDK,版本是 v11.0.100-preview.1

安裝 .NET 11.0 SDK 之後,Visual Studio 2026 的專案範本就會出現 .NET 11 的 target framework 可供挑選。也可以自行修改 .csproj,像這樣:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>

    <!-- 確保目標框架為 .NET 11 -->
    <TargetFramework>net11.0</TargetFramework>

    <!-- 啟用預覽功能 -->
    <EnablePreviewFeatures>true</EnablePreviewFeatures>

    <!-- 開啟 runtime-async 編譯支援 -->
    <Features>$(Features);runtime-async=on</Features>

    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

作為示範,底下是一個簡單的 Console app 的主程式:

using System.Net.Http;
using System.Threading.Tasks;

// 使用 HttpClient 時,宣告為單一實例且可重複使用,避免 Socket 耗盡
using var httpClient = new HttpClient();

Console.WriteLine("使用 async/await 下載網頁並計算字數");

string url = "https://huanlintalk.com";
int count = await DownloadPageAndCountCharsAsync(url);

Console.WriteLine($"網址 {url} 的字數是: {count}");

async Task<int> DownloadPageAndCountCharsAsync(string url)
{
    string content = await httpClient.GetStringAsync(url);
    // 2. 下載完成後,程式碼會從這裡繼續執行
    return content.Length;
}

分別以 .NET 10 和 .NET 11 編譯之後,使用 ILSpy 觀察反組譯之後的 IL code。.NET 10 的版本可以看到編譯器生成的狀態機,包括 MoveNext 方法:


由於反組譯的 IL code 超出螢幕很多,這裡的截圖只呈現其中一部分的程式碼。

.NET 11 編譯的結果則簡短許多,而且已經看不到 MoveNext 方法:


編譯後的 DLL 檔案大小:

  • .NET 10: exe 為 162,816 bytes,dll 是 8,192 bytes。
  • .NET 11: exe 為 148,992 bytes,dll 是 6,144 bytes。

非同步狀態機

雖然 .NET 11 之後,我們可能不用特別在意背後的狀態機是如何處理工作的暫停和接續,但瞭解背後運作的機制也沒有壞處(說不定有助於排解疑難雜症)。因此,這裡也快速複習一下 C# 編譯器替非同步方法產生的狀態機。

本節內容主要是從即將出版的《.NET 非同步程式設計》書籍中搬運過來。(對,經過這麼多年,終於即將「寫完」了。 😝 歡迎追蹤臉書以關注出版消息

當程式執行到「await 一個 Task」時,會發生以下事情:

  1. 檢查 Task 狀態await 會先檢查它所等待的 Task 是否已經完成了。如果已經完成,程式就繼續往下執行,就像沒有 await 一樣。
  2. 暫停與返回:如果 Task 尚未完成,await 會在此處設定一個「恢復點」(continuation),然後立即將控制權返回給呼叫此 async 方法的程式碼。這一步是關鍵!它意味著當前的執行緒沒有被阻塞,可以回去做其他事情。
  3. 恢復執行:當被 await 的 Task 終於完成後,.NET 執行環境(runtime)會回到先前設定的「恢復點」,繼續執行 async 方法中剩下的程式碼。如果 Task 有回傳值 (Task<T>),await 運算式本身就會交出那個 T 型別的結果。

這個「暫停與恢復」的過程,就是由編譯器產生的狀態機在背後管理的。你寫的是看似循序的程式碼,編譯器則幫你把它們 拆解 成多個區塊,並在適當的時機點恢復執行。

如果剛才的解釋過於抽象,可以試著想像一位高效率的廚師(執行緒)正在照著食譜(程式碼)做菜。當他遇到需要把牛排「送進烤箱烤 30 分鐘」的步驟時(遇到 await I/O 操作),他不想要傻傻地站在烤箱前乾等(不想阻塞)。相反地,他在食譜上夾個書籤,記錄目前做到哪個步驟(編譯器建立的狀態機記錄),然後轉身去切小黃瓜或準備另一道菜(執行緒回到 thread pool 接手其他工作)。等烤箱發出「叮」的完成提示聲(Task 完成),他再翻開那個書籤,精準地接續下一個步驟,例如把牛排端出來擺盤(恢復執行)。

當 C# 編譯器遇到 async 方法時,它會在幕後大興土木:為這個方法產生一個實作了 IAsyncStateMachine 介面的隱藏類別,也就是所謂的「狀態機」。

在這個類別中,編譯器會根據你程式碼裡的每一個 await,將整個方法 切割 成不同的狀態(state):

  1. 一開始狀態為 -1。當執行到第一個 await 且發現等待的工作尚未完成時,狀態機就會將當前的狀態設定為 0、儲存當下區域變數的值,並將自己的實例註冊到該 Task 的接續工作(continuation)中,然後直接 return,將執行緒還給呼叫者。
  2. 當背景的 Task 完成時,它會觸發並呼叫狀態機內的 MoveNext() 方法。
  3. MoveNext() 會根據之前記錄的狀態,利用 goto 指令精準跳躍到當初暫停的地方,把變數的值都還原,然後繼續往下執行。

這就是為什麼你可以用「同步的寫法」寫出「非同步的行為」:因為那些你原本必須「手刻」的複雜狀態追蹤與回呼註冊,編譯器產生的狀態機都自動幫你代勞了。

為了更具體了解編譯器做了什麼,這裡用一段大幅簡化過的虛擬碼來模擬一個非同步方法被編譯後所產生的狀態機。請閱讀程式碼中的註解來試著了解其內部的運作邏輯:

// 編譯器產生的狀態機 (簡化過的概念虛擬碼)
class DownloadStateMachine : IAsyncStateMachine
{
    int _state = -1; // 初始狀態為 -1
    string _url;     // 本來方法的傳入參數
    string _content; // 本來的區域變數被提升為類別層級的欄位
    TaskAwaiter<string> _awaiter;

    public void MoveNext()
    {
        // 根據當前狀態,跳轉到對應的程式區塊
        if (_state == 0) goto state_0;

        // --- 狀態 -1 (執行到第一個 await 之前) ---
        _awaiter = httpClient.GetStringAsync(_url).GetAwaiter();

        // 如果工作沒有瞬間做完,就準備暫停
        if (!_awaiter.IsCompleted)
        {
            _state = 0; // 改變狀態,記錄我們停在這裡
            // 告訴 awaiter:「等你下載完,請再次呼叫我的 MoveNext()」
            _awaiter.OnCompleted(this.MoveNext);
            return; // ⭐️ 關鍵:立刻返回,把執行緒還給呼叫者!
        }

    state_0:
        // --- 狀態 0 (從 await 之後恢復執行) ---
        _state = -1; // 恢復預設狀態
        _content = _awaiter.GetResult(); // 取得下載結果

        int result = _content.Length;
        // 將結果放入 Task 中,狀態機執行完畢
        SetResult(result);
    }
}

從這段虛擬碼中,你可以清楚看到 await 邊界是如何將一個原本循序漸進的方法,切斷並轉換成透過 _state 與 goto 來控制執行跳轉的狀態機。這正是 async/await 能夠不阻塞執行緒的底層秘密。

.NET 11 runtime async 其他補充

最後整理 .NET 11 runtime async 的一些相關知識,主要是 AsyncHelpers.Await 和 SynchronizationContext。

核心協調者:AsyncHelpers.Await

在 .NET 11 的全新 runtime async 架構中,AsyncHelpers.Await 扮演著核心協調者 (orchestrator) 與橋樑的關鍵角色。

具體來說,它在新架構中發揮了以下幾個重要作用:

  • 取代龐大的狀態機:過去,當 C# 編譯器遇到 await 關鍵字時,會自動生成包含數百行程式碼的複雜狀態機結構(包含 MoveNext 狀態推進等邏輯)。在新的架構下,編譯器不再生成這些厚重的狀態機,而是大幅簡化 IL (中間語言) 程式碼,改為直接呼叫 AsyncHelpers.Await(...)
  • 連接 Task 物件與底層 runtime:AsyncHelpers 被設計成一種橋樑,負責將 Task(或 ValueTask 等非同步物件)與底層的 .NET runtime 連結起來。
  • 觸發原生的暫停與恢復機制:當編譯出來的程式碼呼叫 AsyncHelpers.Await(...) 時,.NET runtime 會攔截這個呼叫。如果此時 Task 尚未完成,runtime 就會接手處理「暫停」的動作——它只會精準地儲存必要的狀態,並在未來非同步結果準備好時,負責「恢復」該方法的執行。


總結來說,AsyncHelpers.Await 是讓 .NET runtime 能夠原生理解並接管非同步操作的關鍵切入點。透過這個核心方法,不僅讓編譯器產生的程式碼變得極度精簡,更把非同步的狀態管理重責大任直接交還給 runtime 處理,進而開啟了跨方法最佳化、減少記憶體分配等過去難以實現的效能提升機會。

SynchronizationContext

在 .NET 11 的 runtime async 機制下,SynchronizationContext 的運作行為可能會面臨重要的改變:

  • 過去的運作方式(狀態機模式): 過去在使用 async/await 時,編譯器產生的 state machine 會自動捕獲當前的 SynchronizationContext(例如在 WPF 應用程式中負責記住 UI 執行緒),並確保在 await 非同步操作完成後,將後續的延續動作(continuation)發送回原本的 context 中繼續執行。
  • Runtime async 的潛在改變: 在全新的 runtime async 底層機制中,.NET runtime 可能會選擇放棄(drop)捕獲 SynchronizationContext。

根據目前看到的文件,.NET 11 Preview 1 儘管架構上有這些變動,但目前的 .NET 核心函式庫依然是以舊的方式編譯(尚未使用 runtime-async 選項來重新編譯),所以 SynchronizationContext 的實際行為,理論上還是會維持舊有的行為模式。

基於上述情形,如果現在就撰寫效能測試程式來比較新舊機制的效能數據,恐怕也不夠準確。應等到基礎類別庫有使用 runtime-async 選項來重新編譯之後再來測試。

未來隨著後續預覽版推出,核心函式庫如果有使用 runtime async 來編譯,就會需要再確認 SynchronizationContext 的實際行為是否有新的變化。

結語

.NET 11 的 runtime async 是一項重要的架構變革。它將非同步方法的執行與狀態管理從編譯器層級下沉至 runtime 層級,不僅讓編譯後的 IL 程式碼更加精簡,也能夠減少記憶體分配、改善除錯體驗,並且對跨方法呼叫鏈進行最佳化。對開發人員來說,最棒的是不需要修改任何既有的 async/await 程式碼,就能享受到這些好處。

目前這項功能仍處於預覽階段,.NET 基礎類別庫也尚未使用 runtime async 重新編譯,因此現階段的效能數據還不具代表性。此外,SynchronizationContext 在新機制下的行為也值得持續關注。隨著後續預覽版的推出,相關資訊應該會逐漸明朗。

無論如何,從編譯器生成狀態機到 runtime 原生接管非同步流程,這個轉變標誌著 .NET 平台在非同步程式設計領域邁出了重要的一步,是個大好消息。

Keep learning!

See also


沒有留言:

技術提供:Blogger.
回頂端⬆️