Async Programming - 1 async-await 糖的本質(1)

原文出處: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,單步執行

image

image

從調用堆棧看,Start 方法調用了 MoveNext,然後我們的 MoveNext 方法就像 FooAsync 一樣,什麼都沒幹,直接 SetResult 然後返回,然後 Start 返回,FooAsync2 返回,Main 返回,一個沒卵用的異步方法完成了。

這樣就是一個最簡單的異步方法被 rewrite 之後的樣子,這一篇就到這裏,下一篇講講稍微複雜點的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章