2019年7月30日 星期二

C# 對於有使用修飾詞 async 非同步方法內,有沒有使用 await 關鍵字,其這兩種方法會有何差異呢?

C# 對於有使用修飾詞 async 非同步方法內,有沒有使用 await 關鍵字,其這兩種方法會有何差異呢? 

這篇文章主要是要說明,許多人在使用 C# 撰寫程式碼的時候,經常會寫出 async 修飾詞,設計出一個 async 的方法,不過,在這個方法內卻沒有使用到任何的 await 關鍵字。可是,這樣設計出來的 async 方法是不具備非同步運作的功能喔。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

關於 async 的使用說明

若在一個 C# 方法內,若有要等候一個非同步工作或者非同步方法的時候,可以使用 await 關鍵字來等候這些作業,不過,當在方法內加入了 await 運算子關鍵字的時候,就需要在該方法簽章中,加入 async。而且,對於 async 修飾詞若加入到方法簽章上的時候,對於該方法的回傳值就僅能夠是 Task, Task, void。
若原先的方法是一個事件 void MyEventHandler(object sender, EventArgs args),對於 [.NET 標準的事件方法簽章(https://docs.microsoft.com/zh-tw/dotnet/csharp/event-pattern)],就是使用 void 作為回傳值,因此,對於這樣的方法,需要修正為 async void MyEventHandler(object sender, EventArgs args)。對於該方法不是一個 .NET 標準事件,而且是一般的方法且回傳值為 void : void MyMethod(),這個時候需要將 void 修改成為 Task,其完整的寫法為 async Task MyMethod()。最後,當該方法為一般方法且回傳值為非 void 的型別 : string MyMethod(),當想要修正這個方法為非同步方法的時候,就需要在該方法前面加上 async 修飾詞與使用 Task 的泛型表示方式 async Task<string> MyMethod()

一個沒有使用 await 的非同步方法

在這裡將會檢視兩個範例程式碼,這兩個範例程式碼中,都有設計一個 async Task<string> MyMethodAsync() 方法,不過,其中一個在這個方法內是沒有使用 await 的關鍵字,現在,讓我們來看看這兩種的差別。
第一個就是一個沒有使用 await 的非同步方法,請先觀看底下的範例程式碼,在 MyMethodAsync 非同步方法內,是使用沒有任何的 await 關鍵字,這裡將會使用 Thread.Sleep(3000) 來做到同步休息三秒鐘的行為。
因此,請不要使用任何工具去執行這段範例程式碼,直接觀察看看與模擬執行結果,寫下你認為的輸出結果內容,在這裡僅需要寫下輸出結果是 1,2,3,4 這四個數字的排列順序。
底下是這個 async Task<string> MyMethodAsync() async 方法的實際執行結果,若你所思考的執行結果與這個不同,那麼,這代表你的觀念不正確,簡單的說,若一個有使用 async 的非同步方法,若該方法裡面沒有使用到任何的 await 關鍵字,那麼,這個 async 非同步方法就如同是一個同步方法,也就是我們平常在 .NET C# 程式語言中所設計的方法運作順序相同。而且,從底下的執行結果也看到一個重點,那就是整個執行過程中,並沒有任何非同步的效果,因為,所有程式碼都是在同一個執行緒 Thread 下來運行。
因此,這個範例執行結果為 1 3 4 2
1 (1)
進入到非同步方法
3 (1)
準備離開到非同步方法
4 (1)
2 (1)
呼叫非同步方法結果 My Result
Press any key for continuing...
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine($"1 ({Thread.CurrentThread.ManagedThreadId})");
        var task= MyMethodAsync();
        Console.WriteLine($"2 ({Thread.CurrentThread.ManagedThreadId})");

        string result = await task;
        Console.WriteLine($"呼叫非同步方法結果 {result}");

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static async Task<string> MyMethodAsync()
    {
        Console.WriteLine($"進入到非同步方法");
        Console.WriteLine($"3 ({Thread.CurrentThread.ManagedThreadId})");

        Thread.Sleep(3000);

        Console.WriteLine($"準備離開到非同步方法");
        Console.WriteLine($"4 ({Thread.CurrentThread.ManagedThreadId})");

        return "My Result";
    }
}

在 async 非同步方法內使用 await 的執行順序

在第二個範例程式碼與第一個大致完全相同,只不過在 async 非同步方法內,將 Thread.Sleep(3000) 修改成為要使用 await Task.Delay(3000) 表示式。經過這樣修正之後,也就滿足了 async 修飾詞的需求定義,也就是說,要使用到 async 這個修飾詞,該方法內需要有使用到 await 運算子關鍵字。
現在,回想剛剛第一個範例程式碼的執行結果順序內容 : 1 3 4 2,請嘗試觀看底下的程式碼,嘗試自己想像應該是甚麼樣的執行順序,並且請寫下來。
答案似乎有些差異,因為,兩者的輸出內容卻不盡相同,在這個範例中的執行結果為 1 3 2 4,並且看到輸出 4 的時候,所使用到的執行緒與前面三個都不相同;因為這個 async 非同步方法確實是使用非同步的方式來執行,當要等候 await 的工作結束,此時,該async 非同步方法就會立即返回 return 呼叫端,在呼叫端就會繼續往下執行,而在 await 之後的表示式,就會使用非同步的方式來執行相關作業。
一旦非同步工作完成之後,也就是要繼續執行 await 下面的程式碼,因為這是一個 Console 類型專案,所以,將會從執行緒集區 ThreadPool 內取得一個執行緒,繼續往下來執行,這也就是在這裡所看到的結果。
1 (1)
進入到非同步方法
3 (1)
2 (1)
準備離開到非同步方法
4 (4)
呼叫非同步方法結果 My Result
Press any key for continuing...
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine($"1 ({Thread.CurrentThread.ManagedThreadId})");
        var task= MyMethodAsync();
        Console.WriteLine($"2 ({Thread.CurrentThread.ManagedThreadId})");

        string result = await task;
        Console.WriteLine($"呼叫非同步方法結果 {result}");

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
    static async Task<string> MyMethodAsync()
    {
        Console.WriteLine($"進入到非同步方法");
        Console.WriteLine($"3 ({Thread.CurrentThread.ManagedThreadId})");

        await Task.Delay(3000);

        Console.WriteLine($"準備離開到非同步方法");
        Console.WriteLine($"4 ({Thread.CurrentThread.ManagedThreadId})");

        return "My Result";
    }
}

結論與建議

若方法內有使用到 await 關鍵字,要在方法簽章上加入 async 修飾詞,若該方法的回傳值不是 void ,則需要修改回傳值為 Task,若該方法不是一個事件的委派訂閱方法,則需要將 void 替換成為 Task。
若該方法有加入 async 修飾詞,但是裡面卻沒有使用到任何 await 運算子關鍵字,則這個 async 非同步方法,其實就是一個同步方法,並沒有具備非同的運算的效果,而且會有些許的效能損失;這是因為當在這個非同步方法內,不論有沒有使用到 await 關鍵字,只要在方法簽章前面有加上 async修飾詞,在建置這個專案的時候,就會產生該非同步方法需要用到的狀態機類別,而在實際執行這個方法的時候,還是會先產生這個狀態機物件,進行該狀態機的相關初始化設定,接著進入到狀態機內執行,只不過,此時並沒會使用非同步方式來執行,而是使用同步方式執行完成之後,就會離開該狀態機,返回到呼叫端程式碼上。



沒有留言:

張貼留言