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

The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

深入理解 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 的內容如下:

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      // Your code here
    }
  }
}

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

using System;
using System.Threading.Tasks;

namespace ConsoleApp
{
  class Program
  {
    static async Task Main(string[] args)
    {
      // Your code here
    }
  }
}

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

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

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

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

    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace ConsoleApp
    {
      class Program
      {
        static void Main(string[] args)
        {
          Task.Run(async () =>
          {
            using (var http = new HttpClient())
            {
              const string url = "http://docs.microsoft.com/";
              var body = await http.GetStringAsync(url);
              Console.WriteLine($"Size: {body.Length}");
            }
          }).Wait();
        }
      }
    }
    
  2. 切換到 C# 7.1 以上的語言版本

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

    <LangVersion>latest</LangVersion>
    

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

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

    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace ConsoleApp
    {
      class Program
      {
        static async Task Main(string[] args)
        {
          using (var http = new HttpClient())
          {
            const string url = "http://docs.microsoft.com/";
            var body = await http.GetStringAsync(url);
            Console.WriteLine($"Size: {body.Length}");
          }
        }
      }
    }
    

    你也可以透過 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) 當做程式的進入點:

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

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

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

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

相關課程

如果各位有興趣深入理解 C# 非同步程式開發技巧,歡迎報名 2019/6/23、2019/6/30 的【C# 開發實戰:非同步程式開發技巧】課程,我將會用兩天的時間,深入各種 C# / .NET 非同步程式開發技巧,幫助你釐清艱澀難懂的非同步開發觀念!

相關連結