原文出處:Pointer-SMQ的博客,已將所有圖片改爲代碼方便觀看
這一個系列的文章主要來講 C# 中的語言特性 async-await 在語言層面的本質,我們都知道 await 是編譯器進行了一個 rewrite,然而這個 rewrite 並不是直接 rewrite 成其他沒有原生支持 await 的語言的 lambda 回調的形式,而是整個對方法進行了重寫,下面就讓我們來從最簡單的方法,一步一步剖析 await 糖的工作機制。
一個 async 方法,就是你在代碼執行到一半的時候,告訴電腦:我要把函數返回,你先去幹別的事情(比如 UI 操作),等我這邊的事完成之後,再回復現場繼續從剛纔返回的地方執行。方法的執行是依靠狀態機驅動的,一系列的 MoveNext 方法推動狀態機的執行,編譯器則會將方法分而治之,把 async 方法體拆分成許多部分,每一部分是一個狀態機中的狀態,放進 MoveNext 中
新建一個控制檯工程,我們從最簡單的 async 方法開始:FooAsync
using System;
using System.Runtime.CompilerServices;
namespace StateMachineDemo
{
class Program
{
static void Main(string[] args)
{
}
public static async void FooAsync()
{
return;
}
}
}
編譯器已經提示我這個 async 標識符沒卵用了,沒關係
然後我們再寫一個方法來對照被編譯器重寫過的方法:FooAsync2
using System;
using System.Runtime.CompilerServices;
namespace StateMachineDemo
{
class Program
{
static void Main(string[] args)
{
}
public static async void FooAsync()
{
return;
}
public static void FooAsync2()
{
}
}
}
這還沒完,一個異步方法中執行的各項操作,運算,中間暫停然後返回,最後 return 結果,是由狀態機推動的,所以我們手動 rewrite 的 FooAsync2 方法中需要一個狀態機,並且要實現系統的 IAsyncStateMachine 接口。
using System;
using System.Runtime.CompilerServices;
namespace StateMachineDemo
{
class Program
{
static void Main(string[] args)
{
}
public static async void FooAsync()
{
return;
}
public static void FooAsync2()
{
}
public struct FooAsyncStaticMachine : IAsyncStateMachine
{
public void MoveNext()
{
throw new NotImplementException();
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
throw new NotImplementException();
}
}
}
}
state machine 是一個 struct,默認情況下一個 async 方法不需要等待,所以我們不希望有一個在堆上的東西來增加我們的運行負擔。
你可以看到這個接口要求兩個方法,第二個我們之後再講,你可能會奇怪爲什麼一個 IAsyncStateMachine 實例需要 SetStateMachine 另一個實例(後面會說,這其實是他自己),由於我們的 state machine 是一個 struct,所以當方法 await 的時候,整個棧就回退給其他方法來用了,所以你需要把這個 state machine 以及其他的參數轉移到堆上,然後用這個方法來獲得他自己,這個時候堆上的對象的運行時消耗纔是值得的。而 MoveNext 就是之前我們說過的用來推動整個方法運行的方法。
方法裏已經裝了一個 state machine 的實例了,然後我們來看看誰要來驅動着方法一步一步向前走,當控制流回到方法內部的時候他需要來執行一些操作,儘管我們這裏的方法什麼都沒幹,我們還是要把所有的機構都弄好。我們需要一個 AsyncMethodBuilder,這個東西也已經在 System.Runtime.CompilerServices 裏面提供了。
using System;
using System.Runtime.CompilerServices;
namespace StateMachineDemo
{
class Program
{
static void Main(string[] args)
{
}
public static async void FooAsync()
{
return;
}
public static void FooAsync2()
{
var stateMachine = new FooAsyncStaticMachine();
stateMachine.MethodBuilder = new AsyncVoidMethodBuilder();
}
public struct FooAsyncStaticMachine : IAsyncStateMachine
{
public void MoveNext()
{
throw new NotImplementException();
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
throw new NotImplementException();
}
}
}
}
這個 method builder 放在了狀態機內,因爲方法可能返回而我們一直需要它,所以要藉助 state machine 轉移到棧上的同時把它一起轉移了。AsyncVoidMethodBuilder 也是一個 struct,同樣也是我們不希望堆上的東西增加額外的運行時負擔。
method builder 是我們在控制流開始,await,返回的時候應該去使用的東西,這個東西應該在 MoveNext 函數中被使用,這裏我們的方法什麼都沒幹,所以 MoveNext 中只有一個狀態的轉移:開始->返回。返回通過 method builder 的 SetResult 方法完成,所有的 return 都會被 rewrite 成 SetResult。
using System;
using System.Runtime.CompilerServices;
namespace StateMachineDemo
{
class Program
{
static void Main(string[] args)
{
}
public static async void FooAsync()
{
return;
}
public static void FooAsync2()
{
var stateMachine = new FooAsyncStaticMachine();
stateMachine.MethodBuilder = new AsyncVoidMethodBuilder();
}
public struct FooAsyncStaticMachine : IAsyncStateMachine
{
public void MoveNext()
{
MethodBuilder.SetResult();
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
throw new NotImplementException();
}
}
}
}
最後方法怎麼開始呢?我們看看 method builder 有沒有 Start 方法,哈!有。Start 方法需要 state machine 的引用,爲什麼需要,因爲 method builder 需要在方法一步步進行的時候調用 ModeNext,爲什麼要引用,這也說的通,方法的狀態只需要一份,使用引用,避免拷貝,以及 state machine 有可能在棧上(現在這樣)也有可能在堆上,需要用引用來指向它。
using System;
using System.Runtime.CompilerServices;
namespace StateMachineDemo
{
class Program
{
static void Main(string[] args)
{
}
public static async void FooAsync()
{
return;
}
public static void FooAsync2()
{
var stateMachine = new FooAsyncStaticMachine();
stateMachine.MethodBuilder = new AsyncVoidMethodBuilder();
stateMachine.MethodBuilder.Start(ref stateMachine);
}
public struct FooAsyncStaticMachine : IAsyncStateMachine
{
public void MoveNext()
{
MethodBuilder.SetResult();
}
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
throw new NotImplementException();
}
}
}
}
現在的 FooAsync2 就是 async 方法 FooAsync 被 rewrite 之後的樣子。我們在 Main 裏調用 FooAsync2,單步執行
從調用堆棧看,Start 方法調用了 MoveNext,然後我們的 MoveNext 方法就像 FooAsync 一樣,什麼都沒幹,直接 SetResult 然後返回,然後 Start 返回,FooAsync2 返回,Main 返回,一個沒卵用的異步方法完成了。
這樣就是一個最簡單的異步方法被 rewrite 之後的樣子,這一篇就到這裏,下一篇講講稍微複雜點的方法。