2019年5月30日 星期四

.NET 編譯器對 C# 使用了 async / await 關鍵字程式碼,做了什麼事情

.NET 編譯器對 C# 使用了 async / await 關鍵字程式碼,做了什麼事情

測試環境與測試原始碼說明

在這篇文章中,將要來徹底解密 C# 內的非同步工作設計使用到的語法糖 Syntactic sugar,也就是 async 與 await ,為什麼說這是語法糖呢?因為當在 C# 程式語言中使用了這兩個關鍵字之後,C# 編譯器將會產生出與多程式碼,以便可以順利執行非同步計算需求執行所需要的相關程式碼,而這些程式碼對於程式設計師而言,是不需要額外再來撰寫的;也就是說,若沒有這樣的語法糖機制,對於要設計出一個非同步計算的程式碼,是需要再度加入更多需要程式碼。所以,在這篇文章中將會透過這樣的還原技術,了解到當可以在 C# 5.0 以上的版本,可以輕鬆來撰寫 async / await 程式碼,但當使用在 C# 4.0 程式語言版本的時候,因為沒有辦法使用這樣的關鍵字,究竟要透過怎麼樣的程式碼,才能夠時做出這樣的方法。
首先,對於要了解 C# 編譯器對於 async await 做了甚麼事情,需要有一個程式碼並且有進行非同步方法呼叫的程式碼,因此,這篇文章將會使用底下的程式碼作為講解範例。在這個範例程式碼,程式進入點函式 Main 方法,將會以封鎖執行緒的方式等候 MainAsync 方法執行完成,而在 MainAsync 方法內,將會使用 await 運算子等候 MyAsync 這個非同步方法結束,而在 MyAsync 方法內,則會使用 await 運算子,等候 Task.Delay(5000) 這個非同步工作完成,也就是等候 5 秒鐘。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }
    static async Task MainAsync(string[] args)
    {
        await MyAsync();
    }
    static async Task MyAsync()
    {
        await Task.Delay(5000);
        Console.WriteLine("Hello World!");
    }
}
請使用 Visual Studio 建立一個新的主控制台應用專案 Console Application ,將上述的程式碼進行編譯與建置,若沒有發生任何問題,此時可以啟動 ILSpy 來查看這個專案組件的反组譯後的內容。在底下的螢幕截圖將會把 ConsoleApp2 這個組件 Assembly 打開來,並且切換到 [CosoleApp2] > [CosoleApp2] > [Program] 節點下所看到的內容,在 ILSpy 視窗的右半部將會看到 Progam 這個命名空間內使用的 C# 反组譯程式碼內容。
其中,在上面的 ILSpy 視窗中,有兩個紅色圓圈標記,標記1 的地方,可以切換要查看的反组譯內容為 C# 或者使用編譯過後的 IL 中間語言 Intermediate Language 來呈現,甚是可以選擇使用 IL 中間語言 Intermediate Language 與 C# 程式語言來同時顯示。
對於 標記2 的地方,是可以顯示出使用不同的 C# 程式語言版本來顯示
現在可以嘗試看看切換 標記2 的下拉選單 Combobox 控制項,選擇 [C# 4.0 / VS 2010] 這個選項,因為 async / await 這兩個關鍵字是在 C# 5.0 才推出來的功能,若在 C# 4.0 的時候,想要自己設計出這樣的功能,應該要如何撰寫出這樣的程式碼呢?下面螢幕截圖將會是把這個範例原始碼,經過編譯與反组譯之後,使用 C# 4.0 程式語言來呈現的結果。
當點選了 [Program] 節點之後,將右方的 MyAsync 方法展開,竟然看到全然不同的程式碼,甚至相當的陌生,並不瞭解這個 MyAsync 方法內在做了甚麼事情,還有,之前在這個方法內宣告與定義的區域變數與使用 await 等候非同步工作的程式碼,全然都不見了。
另外,從底下螢幕截圖中,可以看到編譯器產生了兩個新的類別,分別是 <MainAsync>d__1 與 <MyAsync>d__2,因為這兩個類別分別有標示 [CompilerGenerated] 屬性宣告,這個屬性是用來 區別編譯器產生的項目與使用者產生的項目,更多資訊可以參考 CompilerGenerated

編譯器 Compiler 處理 1 : 將 async 方法變更成為沒有使用 async 修飾詞與方法改用狀態機物件

首先, 使用 C# 程式語言撰寫的非同步方法,也就是 async Task MyAsync(),其程式碼內容如下所示,這裡有宣告兩個區域變數,一個是 foo 整數型別,另外一個是 message 字串型別,在呼叫非同步方法前,會先針對這兩個區域變數做些初始化定義,並且輸出一段文字,接著會有使用 await 運算子來呼叫與等候非同步工作 Task.Delay 方法結束執行,最後將會做些其他運算與輸出文字。
可是,從上圖中可以看到當使用 ILSpy 反组譯工具所得到的 C# 4.0 的程式碼,對於原先的 async Task MyAsync() 將被修改成為 Task MyAsync() ,也就是 async 這個修飾詞被移除了,另外該 MyAsync 方法裡面的內容也全部被替換了,換成一個很奇怪的類別 <MyAsnyc>d__2 類別所生成的物件, stateMachine 與針對這個 stateMachine 物件所作的相關操作。
其中,這個 <MyAsnyc>d__2 類別,就是編譯器所產生的一個類別,他是一個狀態機 State Machine,從維基百科可以得到這個名詞的定義:有限狀態機(英語:finite-state machine,縮寫:FSM)又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。這裡使用了 AsyncTaskMethodBuilder.Create() 工廠方法建立一個狀態機物件,接著使用這個敘述 stateMachine.<>1__state = -1; 設定該狀態機的狀態值為 -1。最後將會使用 <>t__builder.Start(ref stateMachine);敘述開始啟動這個狀態機,也就是會在這裡進行非同步的相關處理工作,最後將會執行 return stateMachine.<>t__builder.Task; 回到當初呼叫 MyAsync 方法的呼叫端。
C Sharp / C#
static async Task MyAsync()
{
    int foo;
    string message = "";
    foo = 10;
    message = "呼叫非同步方法前";
    Console.WriteLine(message);
    await Task.Delay(5000);
    foo = foo+168;
    message = "呼叫非同步方法後";
    Console.WriteLine(message+ foo.ToString());
}
從上面的敘述與程式碼,可以瞭解到這句話的意思,當程式執行到 await 運算子的時候,將會開始非同步執行所指定的非同步方法,並且立即返回到呼叫端,從這個反组譯的程式就可以一目了然這句話的意義。
不過,此時,await 後面的指定的非同步工作,正在並行或者平行在執行中,而這個 MyAsync 方法回傳一個 Task 物件,若呼叫端一樣有使用 await 運算子來等待這個 MyAsync 非同步工作,此時,因為 MyAsync 也尚未完成,所以呼叫端因為 await 運算子,也會返回到呼叫端 > 呼叫端 的函式內。
只要 await 所指定的非同步工作執行完畢後,就會透過狀態機類別內的程式碼,讓 await 之後的敘述繼續執行,至於為什麼會這樣呢?底下有更完整的介紹。
底下將會是經過編譯器所修改過的 MyAsync 方法,不過將會使用 C# 4.0 的語法來呈現出來,現在可以比對兩個 MyAsync 方法的差異。
C Sharp / C#
private static Task MyAsync()
{
    <MyAsync>d__2 stateMachine = new <MyAsync>d__2();
    stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
    stateMachine.<>1__state = -1;
    AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
    <>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

編譯器 Compiler 處理 1 : async 方法內的變數,將會變成狀態機類別內的欄位

在這裡將會來了解剛剛提到的狀態機類別,他將會做到底下的事情:
  • 使用不同的狀態碼,來處理不同需求,並且使用 MoveNext 進入到狀態機
  • async 方法內的變數,將會變成狀態機類別內的欄位
  • await 關鍵字前後的程式碼,會拆成兩塊區段
  • 使用 callback 來繼續非同步程序後的程式碼
底下的程式碼將會呈現出 MyAsyc 方法所會用到的狀態機類別程式碼,請注意,這個類別 <MyAsync>d__2將會是由編譯器所產生出來的,不是自己撰寫出來的。
C Sharp / C#
private sealed class <MyAsync>d__2 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    private int <foo>5__1;
    private string <message>5__2;
    private TaskAwaiter <>u__1;
    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                <message>5__2 = "";
                <foo>5__1 = 10;
                <message>5__2 = "呼叫非同步方法前";
                Console.WriteLine(<message>5__2);
                awaiter = Task.Delay(5000).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    <>1__state = 0;
                    <>u__1 = awaiter;
                    <MyAsync>d__2 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                <>1__state = -1;
            }
            awaiter.GetResult();
            <foo>5__1 += 168;
            <message>5__2 = "呼叫非同步方法後";
            Console.WriteLine(<message>5__2 + <foo>5__1.ToString());
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult();
    }
    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }
    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

async 方法內的變數,將會變成狀態機類別內的欄位

在這個類別一開始,將會看到底下的欄位宣告,其中,原先在 MyAsync 方法內宣告的區域變數,都會在這裡變成了該類別的欄位 Field,例如原先的 foo 區域整數變數,將會變成 <foo>5__1 欄位,而原先的 message 區域字串變數,將會變成 <message>5__2 欄位。
另外,關於 <>1__state 這個整數欄位,將會表示這個狀態機現在的狀態值,在前一段說明將會得知,這個狀態被建立、啟動之前,這個 <>1__state 狀態值將會為 -1。
C Sharp / C#
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
private int <foo>5__1;
private string <message>5__2;
private TaskAwaiter <>u__1;

使用不同的狀態碼,來處理不同需求,並且使用 MoveNext 進入到狀態機

這個狀態機內的方法 MoveNext ,是整個狀態機運作的主幹地方,只要當這個狀態機的狀態值 <>1__state有所變動的時候,將會再度呼叫 MoveNext 這個方法,在這個方法內,將會依據當時狀態機的狀態值,決定要執行那些程式碼。因此,整個非同步的方法都會反覆透過變更不同的狀態碼,接著呼叫這個 MoveNext 方法。
所以,下在當在開發 .NET 專案程式,若看到例外異常拋出的時候,看到的是有 MoveNext 的函式名稱,不用懷疑,那就是非同步方法中出了問題了。
C Sharp / C#
private void MoveNext()
{
    int num = <>1__state;
    try
    {
        TaskAwaiter awaiter;
        if (num != 0)
        {
            <message>5__2 = "";
            <foo>5__1 = 10;
            <message>5__2 = "呼叫非同步方法前";
            Console.WriteLine(<message>5__2);
            awaiter = Task.Delay(5000).GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                <>1__state = 0;
                <>u__1 = awaiter;
                <MyAsync>d__2 stateMachine = this;
                <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                return;
            }
        }
        else
        {
            awaiter = <>u__1;
            <>u__1 = default(TaskAwaiter);
            <>1__state = -1;
        }
        awaiter.GetResult();
        <foo>5__1 += 168;
        <message>5__2 = "呼叫非同步方法後";
        Console.WriteLine(<message>5__2 + <foo>5__1.ToString());
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <>t__builder.SetException(exception);
        return;
    }
    <>1__state = -2;
    <>t__builder.SetResult();
}

await 關鍵字前後的程式碼,會拆成兩塊區段

現在,還需要繼續來了解上一個階段顯示出來的 MoveNext() 方法程式碼,首先,在這個 MoveNext 方法程式碼中,將會看到原先 MyAsync 的相關程式碼,其中,在 await 關鍵字之前與之後的程式碼將會切割成為兩個區塊,分別使用不同的狀態機之狀態碼來執行。
當第一次啟動與進入到狀態機之後,因為開始的狀態碼為 -1 ,所以,當執行了 MoveNext 方法的時候, if (num != 0) 這個運算是將會得到 true,所以,將會執行 if 成立的程式碼區塊,也就是底下的程式碼。
從這裡可以大致看的出來,在呼叫 awaiter = Task.Delay(5000).GetAwaiter(); 敘述之前的程式碼,就是 MyAsync 原先 C# 程式碼中的 await 關鍵字之前的程式碼;在執行剛剛提到的程式碼,將會進行非同步的呼叫,此時將會進入到休息 5 秒鐘的非同步工作,而且馬上緊接著會檢查該工作是否已經完成,使用 if (!awaiter.IsCompleted) 這個敘述來做到。
會有這樣的設計是因為有些非同步工作因為在某些情況,一呼叫的話,就會立即結束,不會有任何非同步作業執行,所以,當發生這樣的情況,就會繼續執行 if {} else {} 之後的程式碼,立即執行 awaiter.GetResult(); 敘述,得到非同步工作的執行結果,在這裡將會繼續執行原先 MyAsync 方法內的 await 之後的程式碼。
最後會執行 <>1__state = -2; <>t__builder.SetResult(); 這兩個敘述,設定狀態碼為 -2 表示非同步工作結束執行與呼叫 SetResult 方法,設定非同步工作結果。

使用 callback 來繼續非同步程序後的程式碼

若非同步工作尚未完成,變更該狀態機的狀態值為 0 (使用這個敘述 <>1__state = 0;),並且註冊一個 callback 委派方法 (使用這個敘述 <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);),該註冊的委派方法會當非同步工作執行完成之後被呼叫,也就是會再度執行 MoveNext 這個方法。所以,當這個 callback 方法被呼叫的時候,在這個 MoveNext 方法內,因為狀態值為 0,所以,將會執行 else 區塊的程式碼,在這裡,狀態值將會被設定成為 -1,就此結束了這個狀態機運行。
另外,將會繼續執行 就會繼續執行 if {} else {} 之後的程式碼,立即執行 awaiter.GetResult(); 敘述,得到非同步工作的執行結果,在這裡將會繼續執行原先 MyAsync 方法內的 await 之後的程式碼。
最後會執行 <>1__state = -2; <>t__builder.SetResult(); 這兩個敘述,設定狀態碼為 -2 表示非同步工作結束執行與呼叫 SetResult 方法,設定非同步工作結果。
C Sharp / C#
if (num != 0)
{
    <message>5__2 = "";
    <foo>5__1 = 10;
    <message>5__2 = "呼叫非同步方法前";
    Console.WriteLine(<message>5__2);
    awaiter = Task.Delay(5000).GetAwaiter();
    if (!awaiter.IsCompleted)
    {
        <>1__state = 0;
        <>u__1 = awaiter;
        <MyAsync>d__2 stateMachine = this;
        <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
        return;
    }
}
else
{
    awaiter = <>u__1;
    <>u__1 = default(TaskAwaiter);
    <>1__state = -1;
}
awaiter.GetResult();
<foo>5__1 += 168;
<message>5__2 = "呼叫非同步方法後";
Console.WriteLine(<message>5__2 + <foo>5__1.ToString());

當在 MoveNext 方法內遇到例外異常

此時,狀態碼將會設定成為 -2,使用 <>t__builder.SetException(exception); 敘述設定該例外異常,結束這個非同步工作運行。
C Sharp / C#
catch (Exception exception)
{
    <>1__state = -2;
    <>t__builder.SetException(exception);
    return;
}
<>1__state = -2;
<>t__builder.SetResult();

完整的狀態機程式碼

底下是完整的 <MyAsync>d__2 類別所有程式碼
C Sharp / C#
[CompilerGenerated]
private sealed class <MyAsync>d__2 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    private int <foo>5__1;
    private string <message>5__2;
    private TaskAwaiter <>u__1;
    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                <message>5__2 = "";
                <foo>5__1 = 10;
                <message>5__2 = "呼叫非同步方法前";
                Console.WriteLine(<message>5__2);
                awaiter = Task.Delay(5000).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    <>1__state = 0;
                    <>u__1 = awaiter;
                    <MyAsync>d__2 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                <>1__state = -1;
            }
            awaiter.GetResult();
            <foo>5__1 += 168;
            <message>5__2 = "呼叫非同步方法後";
            Console.WriteLine(<message>5__2 + <foo>5__1.ToString());
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult();
    }
    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }
    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

若使用 C# 5.0 / VS 2012 來查看反组譯結果

現在,切換到 C# 5.0 / VS 2012 的模式下,來檢視 Program 命名空間的內容,此時,將會看到與之前使用 C# 4.0 / VS 2010 模式下有些不同,因為,在 C# 5.0 是可以使用 async / await 這兩個關鍵字。
在這裡 MyAsync 非同步方法可以看到完整的程式碼,並且在下圖的右方也看不到狀態機的類別出現在 Program 命名空間內。
現在,切換 ILSpy 上方的下拉選單,切換 C# 成為 IL with C#,並且點選 MyAsync() : Task 節點,將會看到右方出現這個 MyAsync 非同步方法的 IL 程式碼;若慢慢地觀看這個 IL 程式碼,將會看到在這裡將會做到的是建立一個狀態機、設定狀態碼為 -1、啟動狀態機、並且回傳一個工作,也就是切換成為 C# 4.0 / VS 2010 中所看到的 C# 程式碼所做的事情。
底下是 MyAsync 的 IL with C# 程式碼
.method private hidebysig static 
    class [System.Runtime]System.Threading.Tasks.Task MyAsync () cil managed 
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = (
        01 00 21 43 6f 6e 73 6f 6c 65 41 70 70 32 2e 50
        72 6f 67 72 61 6d 2b 3c 4d 79 41 73 79 6e 63 3e
        64 5f 5f 32 00 00
    )
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x20bc
    // Code size 52 (0x34)
    .maxstack 2
    .locals init (
        [0] class ConsoleApp2.Program/'<MyAsync>d__2',
        [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
    )

    IL_0000: newobj instance void ConsoleApp2.Program/'<MyAsync>d__2'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
    IL_000c: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder ConsoleApp2.Program/'<MyAsync>d__2'::'<>t__builder'
    IL_0011: ldloc.0
    IL_0012: ldc.i4.m1
    IL_0013: stfld int32 ConsoleApp2.Program/'<MyAsync>d__2'::'<>1__state'
    IL_0018: ldloc.0
    IL_0019: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder ConsoleApp2.Program/'<MyAsync>d__2'::'<>t__builder'
    IL_001e: stloc.1
    IL_001f: ldloca.s 1
    IL_0021: ldloca.s 0
    IL_0023: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<class ConsoleApp2.Program/'<MyAsync>d__2'>(!!0&)
    IL_0028: ldloc.0
    IL_0029: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder ConsoleApp2.Program/'<MyAsync>d__2'::'<>t__builder'
    IL_002e: call instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
    IL_0033: ret
} // end of method Program::MyAsync
現在,切換 ILSpy 上方的下拉選單,切換 IL with C# 成為 C#,並且點選 <MyAsync>d__2 節點,將會看到右方出現這個 狀態機 C# 程式碼
C Sharp / C#
[CompilerGenerated]
private sealed class <MyAsync>d__2 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    private int <foo>5__1;
    private string <message>5__2;
    private TaskAwaiter <>u__1;
    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                <message>5__2 = "";
                <foo>5__1 = 10;
                <message>5__2 = "呼叫非同步方法前";
                Console.WriteLine(<message>5__2);
                awaiter = Task.Delay(5000).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    <>1__state = 0;
                    <>u__1 = awaiter;
                    <MyAsync>d__2 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                <>1__state = -1;
            }
            awaiter.GetResult();
            <foo>5__1 += 168;
            <message>5__2 = "呼叫非同步方法後";
            Console.WriteLine(<message>5__2 + <foo>5__1.ToString());
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult();
    }

    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

結論

當在 C# 5.0 以上的版本來開發 C# 程式碼的時候,想要執行非同步工作,可以使用 async 修飾詞 與 await 運算子,這樣編譯器就會自動幫助產生出許多在執行非同步工作時候會用到的程式碼,也就是會產生一個狀態機類別。
另外,原先使用 async 修飾詞所設計的非同步方法,將會把函式內的敘述與區域變數,完全轉移到狀態機類別內,並且把這個非同步方法替換成為產生一個有限狀態機物件與進行初始化和啟動狀態機的程式碼。
+

原先的非同步方法內的敘述,將會以 await 運算子作為分隔界線,將其前後敘述拆成兩塊並放在狀態機內,由不同的狀態碼來執行這些程式碼。
因此,async await 這個語法糖在非同步方法呼叫的設計過程中,扮演者非常重要的角色,而且非常的好用。




2019年5月29日 星期三

C# 編譯器對於 async Task Main 方法做了什麼事情,使得可以在 Main 方法內使用非同步方法的 await 關鍵字

C# 編譯器對於 async Task Main 方法做了什麼事情,使得可以在 Main 方法內使用非同步方法的 await 關鍵字

當使用 .NET Framework 或者 .NET Core 建立一個新的 主控台應用程式的時候,若想要在該主控台應用程式的進入點 Entry Point,也就是這個專案一起動的時候,第一個要執行的方法,那就是 static void Main(string[] args) 這個方法;可是,若想要在這個方法內使用 await 關鍵字來等候一個非同步工作的話,編譯器將會提示您有錯誤產生,例如,底下的範例程式碼將會有編譯錯誤。
CS4009%26rd%3Dtrue) 不得同步傳回進入點的 void 或 int
CS5001%26rd%3Dtrue)) 程式未包含適合進入點的靜態 'Main' 方法
其中, CS4009 這個錯誤,透過 Visual Studio 上面的連結並打開使錯誤說明文件,將會看到這樣的說明: 您無法使用async關鍵字,在應用程式進入點 (通常Main方法)。
會有這樣的問題,那是因為可以在 Main 方法上使用 async 這個修飾詞,需要使用 C# 7.1 以上的版本,如同 CS4009 錯誤說明文件上提到的:開頭為C# 7.1 Main方法可以有async修飾詞,因此,想要解決此一錯誤,那就是要將這個專案設定為使用 C# 7.1 以上的版本。
而在 CS5001 的錯誤說明網頁中,將會看到這樣的內容:程式 'program' 未包含適合進入點的靜態 'Main' 方法。當沒有靜態時,會發生此錯誤Main產生可執行檔的程式碼中找不到具有正確的簽章的方法。
根據 CS5001 錯誤說明文件中提到的:當沒有靜態時,會發生此錯誤Main產生可執行檔的程式碼中找不到具有正確的簽章的方法。那是因為有了 CS4009 的錯誤,在 Main 方法中使用了 async 是不合法的,導致編譯器無法找到正確、合法的 Main 簽章方法。
可是,若不能夠在 Main 方法內使用 await 關鍵字 (因為,不想要使用封鎖執行緒 Block Thread 的做法),這要如何做到當要等候一個非同步工作的時候,可以立即返回到原先呼叫端那裏去這樣的需求呢?
C Sharp / C#
class Program
{
    static async void Main(string[] args)
    {
        await Task.Delay(1000);
        Console.WriteLine("Hello World!");
    }
}
要解決此一問題,請在方案總管內,使用滑鼠右擊該主控台應用程式專案的節點,點選 [屬性] 項目;當該專案的屬性視窗出現之後,切換到 [建置] 標籤頁次,在該標籤頁次最下方,會有一個 [進階] 按鈕,請點選此一按鈕;如下面螢幕截圖所示:
現在,[進階建置設定] 對話窗將會出現,請點選該對話窗的 [語言版本] 這個欄位旁邊的下拉選單控制項,這個下拉選單控制項原先預設的選項為 [c# 支援的最新次要版本],請在這個下拉選單中選擇 C# 7.1 以上的版本,在這裡將會選取 C# 7.3。完成後,在 [進階建置設定] 對話窗右下方點選 [確定] 按鈕,關閉此對話窗
操作方式摘要:[打開專案屬性視窗] > [建置] 標籤頁次 > [進階] 按鈕 > [語言版本] 7.3 > [確定] 按鈕
當經過這樣修正之後,再度重新進行專案建置, CS4009 的錯誤訊息消失了,而 CS5001 的錯誤訊息依然存在,這是因為還需要修正 Main 方法的宣告,把 void 修改成為 Task,也就是這樣 static async Task Main(string[] args) 這樣的目的是要讓 Main 這個方法成為一個非同步的方法,所以,這個 Main 方法將會回傳一個 Task 物件,因此,當執行到 await Task.Delay(1000); 敘述之後,就可以立即返回到原先呼叫 Main 方法的呼叫端。
現在,再度重新建置該專案,就會發現到所有的錯誤都消失了。
會需要這樣做,這是因為 C# 要求這樣做,需要一個 async Task Main 的方法,可是,這也產生另外一個問題,當在 Main 方法內執行敘述的時候,遇到了 await 關鍵字,將會立即返回到呼叫端,而當初呼叫該 Main 方法的呼叫端,也就是開始執行該程式的一開始處,如此,那不就造成了該程式就會結束執行了呀。可是,當實際執行該專案,卻發現到該專案真的會停留 1秒鐘後,才會顯示出一段字串內容,並沒有遇到 await 關鍵字之後,就立即結束執行了。
想要知道為什麼會發生這樣的問題,那就需要使用 .NET 組件 Assembly 反组譯工具,查看編譯器到底做了甚麼事情,以及真正要執行的程式碼變成如何?所以,在這裡將會透過 ILSpy 這套 .NET 組件反组譯工具來進行深度了解,想要取得 ILSpy 這套工具,可以從 ILSpy 這裡免費取得。
當下載完成 ILSpy 這套工具後,這裡下載的是 .zip 檔案格式,下載完成之後,可以解開該壓縮檔案到任何一個電腦上的目錄,接著在解壓縮目錄內找到 ILSpy.exe 這個檔案,請執行這個檔案。底下螢幕截圖為執行 ILSpy 工具的畫面。
如此,可以點選 ILSpy 功能表 [File] > [Open...] ,接著,切換到剛剛建立專案的 bin\Debug 目錄下,找到該專案的組件檔案名稱,開啟這個組件檔案;現在, ILSpy 將會顯示出這個組件中有哪些類別、方法與相關程式碼,如下圖所示。
在這裡建立的專案名稱為 ConsoleApp2,展開這個節點,將會看到 Program 命名空間下會有 Base Type、 Derived Typed 、 <Main>d__0 、 Program() 、 <Main>(string[]) : void 、 Main(string[]) : Task 這些傑點出現;現在又發現到一個怪異點,那就是這個範例程式僅有一個 Program 命名空間與 Main 方法,那麼,為什麼從 ILSpy 上看到這麼多其他的項目出現。這些原先程式碼沒有的項目,都是編譯器自動幫忙產生的,這是因為要能夠處理、讓這個程式可以使用非同步的 await 方法呼叫。
若看不到如上圖的畫面內容,請點選 ILSpy 功能表 [View] > [Show all types and members] 選項,就可以看到這些內容。
好的,接下來進行剖析,了解到底出了甚麼樣的變化,首先,在 C# 原始碼寫的 static async Task Main(string[] args) 方法,將會出現在上圖 標記1 的節點,其實,這個方法僅僅是一個非同步的方法,並不是這個程式的進入點,那麼,這隻程式的進入點在哪裡呢?請查看 標記2 的節點,也就是 <Main>(string[]) : void ,這就是這個程式的進入點,不過,編譯器將原先程式進入點的 Main 方法,改成使用 <Main> 方法;請注意,該方法的回傳值也是 void 喔。
而在 標記3 的節點,這個類別是編譯器產生出來的,這是因為有使用到 Main(string[]) : Task 方法,編譯器將會產生相對應的非同步處理狀態機所需要用到的相關程式碼。
到現在為止,原本單純 C# 程式碼中的Main 方法 ,經過建置之後,產生出這麼多的程式碼,接下來將逐步了解這些程式碼做了甚麼事情。
首先,來看看編譯過後的專案進入點,也就是 <Main>(string[]) : void ;請在 ILSpy 中點選這個節點,將會看到如下圖畫面,其該程式進入點的程式碼經過反组譯後,將會如同底下程式碼列表。
當這個程式一開始啟動之後,將會進入到這個進入點方法( <Main>)來執行,這個方法內僅有一行敘述,一開始將會呼叫 Main(args) 方法,而這個方法就是使用 C# 所設計的非同步方法,因為是非同步方法,將會回傳一個 Task 物件,所以,可以呼叫 Task.GetAwaiter 方法,該方法將會取得用來等候這個 Task 的 awaiter。而且,這個方法僅供編譯器使用,而不是應用程式程式碼中使用。
當取得了 TaskAwaiter 物件之後(因為 GetAwaiter() 方法會回傳這個型別的物件), 就可以再度呼叫 TaskAwaiter.GetResult 。這個方法將會結束對非同步工作完成的等候。此 API 支援此產品基礎結構,但無法直接用於程式碼之中。也就是說,這個方法將會由編譯器來使用,不建議在自己的 C# 程式碼中來使用。
講白話一點,這一行敘述,就是要呼叫 Main 非同步方法,並且使用封鎖執行緒的方式,等候非同步方法結束完成,因此,當在 C# 程式碼中的 static async Task Main(string[] args) 方法內有使用到 await 關鍵字之後,將會返回到原先呼叫端,也就是這個 <Main> 方法,而這個方法將會等候到該非同步方法完全結束之後,才會回傳到原先呼叫端,也就是結束執行,所以,遇到有 await 關鍵字之後,並不會造成該程式結束執行。
C Sharp / C#
// ConsoleApp2.Program
private static void <Main>(string[] args)
{
    Main(args).GetAwaiter().GetResult();
}
那麼,為什麼編譯器要在產生一個 <Main>(string[]) : void 方法呢?因為原先在 C# 程式碼中,已經將原先的 void Main 方法改寫成為一個非同務的方法,static async Task Main(string[] args),使得原先 C# 內的程式進入點函式不見了,所以,C# 編譯器要產生一個新的 Main 函式,作為這個程式的進入點要呼叫的函式,但是,若這個編譯器產生的進入點函式名稱命名為 Main(string[]) : void,將會與C#寫的 Main(string[]) : Task 方法會視為相同,也就是同一個方法,這樣會產生錯誤;會有這樣的錯誤,這是因為在 C# 內,判斷一個類別內的方法,其 方法名稱 / 參數型別與數量 (這是函式特徵的定義) 都要相同,就會視為兩個相同的方法,而不會因為其回傳值的型別不同,而是為不同的函數。所以,需要重新命名一個新的函式名稱,作為此程序的進入點要呼叫的函數。
最後,若不想要變更 [進階建置設定] 對話窗內的 [語言版本] 下拉選單控制項值,此時,可以參考使用底下的程式碼寫法,也會達成相同的執行結果的,也就是可以在 Console App 內輕鬆使用 await 來呼叫非同步方法。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }
    static async Task MainAsync(string[] args)
    {
        await Task.Delay(5000);
        Console.WriteLine("Hello World!");
    }
}