前言
這篇文章的開頭,筆者想多說兩句,不過也是為了以后再也不多嘴這樣的話。
在日常工作中,筆者接觸得最多的開發工作仍然是在 .NET Core 平台上,當然因為團隊領導的開放性和團隊風格的多樣性(這和 CTO 以及主管的個人能力也是分不開的),業界前沿的技術概念也都能在上手的項目中出現。所以雖然現在團隊仍然處於疾速的發展中,也存在一些奇奇怪怪的事情,工作內容也算有緊有松,但是總體來說也算有苦有樂,不是十分排斥。
其實這樣的環境有些類似於筆者心中的“聖地” Thoughtworks 的 雛形(TW的HR快來找我啊),筆者和女朋友談到自己最想做的工作也是技術咨詢。此類技術咨詢公司的開發理念基本可以用一句概括:遵循可擴展開發,可快速迭代,可持續部署,可的架構設計,追求目標應用場景下最優於團隊的技術選型決策。
所以語言之爭也好,平台之爭也好,落到每一個對編程和解決問題感興趣的開發者身上,便成了最微不足道的問題。能夠感受不同技術間的碰撞,領略到不同架構思想中的精妙,就已經是一件滿足的事情了,等到團隊需要你快速應用其他技術選型時,之前的努力也是助力。當然面向工資編程也是一種取舍,筆者思考的時候也會陷入這個怪圈,所以希望在不斷的學習和實踐中,能夠讓自己更滿意吧。
著名的 DRY 原則告訴我們 —— Don't repeat yourself,而筆者想更進一步的是,Deep Dive And Wide Mind,深入更多和嘗試更多。
奇怪的前言就此結束。
作為最新的正式版本,雖然版本號只是小小的提升,但是 .NET Core 2.1 相比 .NET Core 2.0 在性能上又有了大大的提升。無論是項目構建速度,還是字符串操作,網絡傳輸和 JIT 內聯方法性能,可以這么說的是,如今的 .NET Core 已經主動為開發者帶來摳到字節上的節省體驗。具體的介紹還請參看 Performance Improvements in .NET Core 2.1 。
而在這篇文章里,筆者要聊聊的只是關於 async/await 的一些底層原理和 .NET Core 2.1 在異步操作對象分配上的優化操作。
async/await 實現簡介
熟悉異步操作的開發者都知道,async/await 的實現基本上來說是一個骨架代碼(Template method)和狀態機。
從反編譯器中我們可以窺見骨架方法的全貌。假設有這樣一個示例程序
internal class Program
{
private static void Main()
{
var result = AsyncMethods.CallMethodAsync("async/await").GetAwaiter().GetResult();
Console.WriteLine(result);
}
}
internal static class AsyncMethods
{
internal static async Task<int> CallMethodAsync(string arg)
{
var result = await MethodAsync(arg);
await Task.Delay(result);
return result;
}
private static async Task<int> MethodAsync(string arg)
{
var total = arg.First() + arg.Last();
await Task.Delay(total);
return total;
}
}
為了能更好地顯示編譯代碼,特地將異步操作分成兩個方法來實現,即組成了一條異步操作鏈。這種“侵入性”傳遞對於開發其實是更友好的,當代碼中的一部分采用了異步代碼,整個傳遞鏈條上便不得不采用異步這樣一種正確的方式。接下來讓我們看看編譯器針對上述異步方法生成的骨架方法和狀態機(也已經經過美化產生可讀的C#代碼)。
[DebuggerStepThrough]
[AsyncStateMachine((typeof(CallMethodAsyncStateMachine)]
private static Task<int> CallMethodAsync(string arg)
{
CallMethodAsyncStateMachine stateMachine = new CallMethodAsyncStateMachine {
arg = arg,
builder = AsyncTaskMethodBuilder<int>.Create(),
state = -1
};
stateMachine.builder.Start<CallMethodAsyncStateMachine>(
(ref stateMachine)=>
{
// 骨架方法啟動第一次 MoveNext 方法
stateMachine.MoveNext();
});
return stateMachine.builder.Task;
}
[DebuggerStepThrough]
[AsyncStateMachine((typeof(MethodAsyncStateMachine)]
private static Task<int> MethodAsync(string arg)
{
MethodAsyncStateMachine stateMachine = new MethodAsyncStateMachine {
arg = arg,
builder = AsyncTaskMethodBuilder<int>.Create(),
state = -1
};
// 恢復委托函數
Action __moveNext = () =>
{
stateMachine.builder.Start<CallMethodAsyncStateMachine>(ref stateMachine);
}
__moveNext();
return stateMachine.builder.Task;
}
- MethodAsync/CallMethodAsync - 骨架方法
- MethodAsyncStateMachine/CallMethodAsyncStateMachine - 每個 async 標記的異步操作都會產生一個骨架方法和狀態機對象
- arg - 顯然原始代碼上有多少個參數,生成的代碼中就會有多少個字段
- __moveNext - 恢復委托函數,對應狀態機中的 MoveNext 方法,該委托函數會在執行過程中作為回調函數返回給對應Task的 Awaiter 從而使得 MoveNext 持續執行
- builder - 該結構負責連接狀態機和骨架方法
- state - 始終從 -1 開始,方法執行時狀態也是1,非負值代表一個后續操作的目標,結束時狀態為 -2
- Task - 代表當前異步操作完成后傳播的任務,其內包含正確結果
可以看到,每個由 async 關鍵字標記的異步操作都會產生相應的骨架方法,而狀態機也會在骨架方法中創建並運行。以下是實際的狀態機內部代碼,讓我們用實際進行包含兩步異步操作的 CallMethodAsyncStateMachine 做例子。
[CompilerGenerated]
private sealed class CallMethodAsyncStateMachine : IAsyncStateMachine
{
public int state;
public string arg; // 代表變量
public AsyncTaskMethodBuilder<int> builder;
// 代表 result
private int result;
// 代表 var result = await MethodAsync(arg);
private Task<int> firstTaskToAwait;
// 代表 await Task.Delay(result);
private Task secondTaskToAwait;
private void MoveNext()
{
try
{
switch (this.state) // 初始值為-1
{
case -1:
// 執行 await MethodAsync(arg);
this.firstTaskToAwait = AsyncMethods.MethodAsync(this.arg);
// 當 firstTaskToAwait 執行完畢
this.result = firstTaskToAwait.Result;
this.state = 0;
// 調用 this.MoveNext();
this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
case 0:
// 執行 Task.Delay(result)
this.secondTaskToAwait = Task.Delay(this.result);
// 當 secondTaskToAwait 執行完畢
this.state = 1;
// 調用 this.MoveNext();
this.builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
case 1:
this.builder.SetResult(result);
return;
}
}
catch (Exception exception)
{
this.state = -2;
this.builder.SetException(exception);
return;
}
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
}
可以看到一個異步方法內含有幾個異步方法,狀態機便會存在幾種分支判斷情況。根據每個分支的執行情況,再通過調用 MoveNext 方法確保所有的異步方法能夠完整執行。更進一步,看似是 switch 和 case 組成的分支方法,實質上仍然是一條異步操作執行和傳遞的Chain。
上述的 CallMethodAsync 方法也可以轉化成以下 Task.ContinueWith 形式:
internal static async Task<int> CallMethodAsync(string arg)
{
var result = await (
await MethodAsync(arg).ContinueWith(async MethodAsyncTask =>
{
var methodAsyncTaskResult = await MethodAsyncTask;
Console.Write(methodAsyncTaskResult);
await Task.Delay(methodAsyncTaskResult);
return methodAsyncTaskResult;
}));
return result;
}
可以這樣理解的是,總體看來,編譯器每次遇到 await,當前執行的方法都會將方法的剩余部分注冊為回調函數(當前 await 任務完成后接下來要進行的工作,也可能包含 await 任務,仍然可以順序嵌套),然后立即返回(return builder.Task)。 剩余的每個任務將以某種方式完成其操作(可能被調度到當前線程上作為事件運行,或者因為使用了 I/O 線程執行,或者在單獨線程上繼續執行,這其實並不重要),只有在前一個 await 任務標記完成的情況下,才能繼續進行下一個 await 任務。有關這方面的奇思妙想,請參閱《通過 Await 暫停和播放》
.NET Core 2.1 性能提升
上節關於編譯器生成的內容並不能完全涵蓋 async/await 的所有實現概念,甚至只是其中的一小部分,比如筆者並沒有提到可等待模式(IAwaitable)和執行上下文(ExecutionContext)的內容,前者是 async/await 實現的指導原則,后者則是實際執行異步代碼,返回給調用者結果和線程同步的操控者。包括生成的狀態機代碼中,當第一次執行發現任務並未完成時(!awaiter.isCompleted),任務將直接返回。
主要原因便是這些內容講起來怕是要花很大的篇幅,有興趣的同學推薦去看《深入理解C#》和 ExecutionContext。
異步代碼能夠顯著提高服務器的響應和吞吐性能。但是通過上述講解,想必大家已經認識到為了實現異步操作,編譯器要自動生成大量的骨架方法和狀態機代碼,應用通常也要分配更多的相關操作對象,線程調度同步也是耗時耗力,這也意味着異步操作運行性能通常要比同步代碼要差(這和第一句的性能提升並不矛盾,體重更大的人可能速度降低了,但是抗擊打能力也更強了)。
但是框架開發者一直在為這方面的提升作者努力,最新的 .NET Core 2.1 版本中也提到了這點。原本的應用中,一個基於 async/await 操作的任務將分配以下四個對象:
- 返回給調用方的Task
任務實際完成時,調用方可以知道任務的返回值等信息 - 裝箱到堆上的狀態機信息
之前的代碼中,我們用了ref
標識一開始時,狀態機實際以結構的形式存儲在棧上,但是不可避免的,狀態機運行時,需要被裝箱到堆上以保留一些運行狀態 - 傳遞給Awaiter的委托
即前文的_moveNext
,當鏈中的一個 Task 完成時,該委托被傳遞到下一個 Awaiter 執行 MoveNext 方法。 - 存儲某些上下文(如ExecutionContext)信息的狀態機執行者(MoveNextRunner)
據 Performance Improvements in .NET Core 2.1 一文介紹:
for (int i = 0; i < 1000; i++)
{
await Yield();
async Task Yield() => await Task.Yield();
}
當前的應用將分配下圖中的對象:
而在 .NET Core 2.1 中,最終的分配對象將只有:
四個分配對象最終減少到一個,分配空間也縮減到了過去的一半。更多的實現信息可以參考 Avoid async method delegate allocation。
結語
本文主要介紹了 async/await 的實現和 .NET Core 2.1 中關於異步操作性能優化的相關內容。因為筆者水平一般,文章篇幅有限,不能盡善盡美地解釋完整,還希望大家見諒。
無論是在什么平台上,異步操作都是重要的組成部分,而筆者覺得任何開發者在會用之余,都應該更進一步地適當了解背后的故事。具體發展中,C# 借鑒了 F#中的異步實現,其他語言諸如 js 可能也借鑒了 C# 中的部分內容,當然一些基本術語,比如回調或是 feature,任何地方都是相似的,怎么都脫離不開計算機體系,這也說明了編程基礎的重要性。