深入理解 C# 7.1 提供的 async 非同步 Main() 方法

我們在開發 .NET 應用程式的時候,預設選取的 C# 語言版本為「最新已發行主要版本」(latest major version),如果以 Visual Studio 2017 v15.9.10 來說,內建的 C# 最新發行版本就是 7.3 版,因此主要版本就是 7.0 版。本篇文章要來介紹 C# 7.1 提供的一個語法糖,它能讓你的 Console 應用程式,將主程式的進入點 Main() 方法也能宣告為非同步(async)的版本,好讓你從頭到尾都用非同步的方式開發應用程式,最後還會說明這個新語法背後的技術原理。

 

預設的 Console 應用程式

通常剛建立好一個 Console 應用程式專案,其主程式 Program.cs 的內容如下:

 

 1 using System;
 2 using System.Threading.Tasks;
 3 
 4 namespace ConsoleApp
 5 {
 6   class Program
 7   {
 8     static void Main(string[] args)
 9     {
10       // Your code here
11     }
12   }
13 }

由於 Visual Studio 2017 預設主控台應用程式的專案範本,預設僅支援到 C# 7.0 版,也就是說,你沒辦法寫出以下的程式碼,專案建置的時候會遇到 error CS5001: 程式未包含適合進入點的靜態 'Main' 方法 與 CS8107: C# 7.0 中未提供功能 '非同步主要'。請使用語言版本 7.1 或更高版本。 的錯誤訊息:

 1 using System;
 2 using System.Threading.Tasks;
 3 
 4 namespace ConsoleApp
 5 {
 6   class Program
 7   {
 8     static async Task Main(string[] args)
 9     {
10       // Your code here
11     }
12   }
13 }

使用 async/await 開發 Console 應用程式

解決這個問題還挺簡單,基本上有兩種方法:

  1. 呼叫 Task.Run(async () => { ... }).Wait(); 即可

    以下用一段簡單的 HttpClient 範例示範寫法:

 1 using System;
 2 using System.Net.Http;
 3 using System.Threading.Tasks;
 4 
 5 namespace ConsoleApp
 6 {
 7   class Program
 8   {
 9     static void Main(string[] args)
10     {
11       Task.Run(async () =>
12       {
13         using (var http = new HttpClient())
14         {
15           const string url = "http://docs.microsoft.com/";
16           var body = await http.GetStringAsync(url);
17           Console.WriteLine($"Size: {body.Length}");
18         }
19       }).Wait();
20     }
21   }
22 }

2. 切換到 C# 7.1 以上的語言版本

無論你用 .NET Framework 或 .NET Core 的 Console App,都可以直接手動修改 *.csproj 專案檔,只要在第一個 <PropertyGroup> 底下加入 <LangVersion>latest</LangVersion> 即可:

1 <LangVersion>latest</LangVersion>

強烈建議手動編輯 *.csproj 加入 <LangVersion> 設定。

成功加入之後,就可以直接以 async Task 來宣告 Main() 方法,以下用一段簡單的 HttpClient 範例示範寫法:

 

 1 using System;
 2 using System.Net.Http;
 3 using System.Threading.Tasks;
 4 
 5 namespace ConsoleApp
 6 {
 7   class Program
 8   {
 9     static async Task Main(string[] args)
10     {
11       using (var http = new HttpClient())
12       {
13         const string url = "http://docs.microsoft.com/";
14         var body = await http.GetStringAsync(url);
15         Console.WriteLine($"Size: {body.Length}");
16       }
17     }
18   }
19 }

你也可以透過 msbuild /p:LangVersion=latest 命令,自動覆寫 csproj 檔案中的預設值。

 

深入 static async Task Main(string[] args) 的技術細節

光是調整設定好像有點無趣,我們來看看套用 C# 7.1 的 async Main 之後,C# 編譯器在背後偷偷做了什麼事!

我先將上一段程式碼進行編譯,產生 ConsoleApp.exe 執行檔,並且用 ILSpy 進行反組譯分析。由於 C# 7.1 在編譯這段程式碼時,會自動對程式碼進行調整,之前有提過 C# 7.1 的 async Main 其實是個語法糖,也就是說程式碼在編譯完成後,將會跟原本的不太一樣。

在進行分析之前,請先進行以下設定,開啟 Show all types and members 選項:

我們要進行反組譯分析的第一步,就是找到程式的進入點。此時你會發現,原來 C# 7.1 並沒有真的將 static async Task Main(string[] args) 當成程式的進入點,而是另外建立一個 傳統的 Main() 方法,以這個 private static void <Main>(string[] args) 當做程式的進入點:

 

1 private static void <Main>(string[] args)
2 {
3     Main(args).GetAwaiter().GetResult();
4 }

這裡的 <Main> 方法是由編譯器自動產生的方法,他呼叫了一個 Main() 方法,這個正是你在 Visual Studio 中撰寫的主程式,其回傳型別是 Task。然後直接呼叫了 Task.GetAwaiter Method 取得 TaskAwaiter,然後再呼叫 TaskAwaiter.GetResult Method 結束對非同步工作完成的等候。

請注意:GetAwaiter() 與 GetResult() 都不建議自己寫在應用程式中,這兩個方法僅供編譯器使用。

原始碼本身可以解釋一切,如果有興趣的話,可以進一步查看 <Main>d__0 這個類別的內容,這是一份 IAsyncStateMachine 的實作,用來控制 async/await 的執行流程,必要的時候還要翻開 IL 才能看懂整個執行過程。我相信透過這個反組譯的過程,可以幫助開發人員更加理解 async/await 的技術細節與設計原理,也能更能有自信的撰寫非同步的程式碼。

 

posted @ 2020-03-07 18:02  TonysDad  阅读(661)  评论(0编辑  收藏  举报