談C# 編譯器編譯前的程式碼擴展行為 (2017年續 上)

這是2011年 談C# 編譯器編譯前的程式碼擴展行為 的續篇,當年該文章由C# 1.0討論到4.0,中間也過了好多年,今年終於興起來寫個續篇了,如果你沒看過前篇,建議看這篇前先瀏覽一下,該文中提及的東西至今仍然未過時,語言的東西不比特定技術,很少會發生Breaking Changes或是整個Feature移除,頂多只是改善。

導讀

  這是2011年 談C# 編譯器編譯前的程式碼擴展行為 的續篇,當年該文章由C# 1.0討論到4.0,中間也過了好多年,今年終於興起來寫個續篇了,如果你沒看過前篇,建議看這篇前先瀏覽一下,該文中提及的東西至今仍然未過時,語言的東西不比特定技術,很少會發生Breaking Changes或是整個Feature移除,頂多只是改善,如果對Task及Thread Pool不熟,可以參考 The Parallel Programming Of .NET Framework 4.0(2) -Task Library這系列。

  這次續篇我沒打算一次寫完,大概會分成5.0、6.0/7.0三篇,不過間隔應該不會太久,因為相較於6.0/7.0,5.0才是最複雜的。

 

C# 5.0

  C# 5.0新增兩個語言特性,一個是async/await語法,另一個是Caller Information,Caller Information屬於Runtime與Attribute的合作,並不算程式碼擴展,所以本文不會提及。

 

async/ await

  C# 4.0的主軸是Dynamic Programming,C# 5.0的主軸則是Asynchronous Programming,主要目的在於將C# 打造成非同步語言,所謂的非同步指的是讓設計師可以藉助語言及平台的支援,讓程式充分運用多核心CPU的功能,最大幅度降低無謂的阻塞狀態,呃…..白話點說就是讓你的程式可以在同樣的時間內處理更多工作,當然,前提是你得先搞懂async/await是怎麼回事,內部是如何運作,那怎麼樣叫做通透呢? 很簡單,當你看到一段async/await程式碼時,能正確的描述其中的脈絡,這很簡單是嗎? 至少對我而言,這東西一點都不簡單。讓我們由最簡單的一個Console例子開始看。

public static async void DoWork1()
{
    HttpClient client = new HttpClient();
    var content = await client.GetStringAsync("http://www.google.com");
    if (content.Contains("body"))
        Console.WriteLine("found");
    else
        Console.WriteLine("not found");
}

過了這麼多年,相信看不懂這程式片段的人不多,這是標準的async/await寫法,那麼這個程式與下面這個程式有不同嗎?

public static void DoWork2()
{
    WebClient client = new WebClient();
    var content = client.DownloadString("http://www.google.com");
    if (content.Contains("body"))
        Console.WriteLine("found");
    else
        Console.WriteLine("not found");
}

大概98%以上的人都知道,DoWork2是一個阻塞行為,因為client.DownloadString會等待網路資料的回傳,DoWork1則是藉助await,不等待,但她如何達到不等待,脈絡如何? 我們等下再談。

接著是下面這個程式,很貼近DoWork1,幾近是DoWork1的非async版。

public static void DoWork3()
{
     WebClient client = new WebClient();
     client.DownloadStringCompleted += (s, args)=>
     {
         var content = args.Result;
         if (content.Contains("body"))
             Console.WriteLine("found");
         else
             Console.WriteLine("not found");
     };
     client.DownloadStringAsync(new Uri("http://www.google.com"));
}

你真的可以退100步想,DoWork1 = DoWork3。接著是下面這個。

public static void DoWork4()
{
     WebRequest wq = WebRequest.Create("http://www.google.com");
     wq.BeginGetResponse((state) =>
     {
         var resp = wq.EndGetResponse(state);
         using (var sr = new StreamReader(resp.GetResponseStream()))
         {
            var content = sr.ReadToEnd();
            if (content.Contains("body"))
                Console.WriteLine("found");
            else
                Console.WriteLine("not found");
         }
     }, null);
}

請問,你可以回答出DoWork3 與 DoWork4的脈絡是相同的嗎? 等下分曉,再看一個。

public static void DoWork5()
{
    HttpClient client = new HttpClient();            
    client.GetStringAsync("http://www.google.com").ContinueWith((result) =>
    {
        if (result.Result.Contains("body"))
            Console.WriteLine("found");
        else
            Console.WriteLine("not found");
    });
}

DoWork5與DoWork1是一樣的嗎? DoWork5與DoWork4是一樣的嗎?林志玲跟波多野差在哪?  (呃,跳tone了….)。

其實它們的結果都是一樣的,但脈絡不同,簡單的說,就是運用Thread的手法不同,也因為是這些不同,才使得async/await強大,但也使得其變得複雜。

讓我們利用Thread ID來追蹤它們的脈絡,首先是DoWork1_1(DoWork1)。

public static async void DoWork1_1()
{
     HttpClient client = new HttpClient();
     Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId);
     var content = await client.GetStringAsync("http://www.google.com");
     Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId);
     if (content.Contains("body"))
         Console.WriteLine("found");
     else
         Console.WriteLine("not found");
}

結果是這樣。

before:9

after:15

found

由await為分界,切開成為兩個Thread,這是編譯器擴展手法所致,我們後面再談。再看DoWork2的版本。

public static void DoWork2_1()
{
    WebClient client = new WebClient();
    Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId);
    var content = client.DownloadString("http://www.google.com");
    Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId);
    if (content.Contains("body"))
        Console.WriteLine("found");
    else
        Console.WriteLine("not found");
}

這是阻塞版本,所以兩個Thread一定是一樣的。

before:9

after:9

found

看DoWork3。

public static void DoWork3_1()
{
      WebClient client = new WebClient();
      Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId);
      client.DownloadStringCompleted += (s, args) =>
      {
          Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId);
          var content = args.Result;
          if (content.Contains("body"))
              Console.WriteLine("found");
          else
              Console.WriteLine("not found");
      };
      client.DownloadStringAsync(new Uri("http://www.google.com"));
}

before:9

after:21

found

一樣是兩條Thread,看DoWork4。

public static void DoWork4_1()
{
     WebRequest wq = WebRequest.Create("http://www.google.com");
     Console.WriteLine("before:" + 
              Thread.CurrentThread.ManagedThreadId);
     wq.BeginGetResponse((state) =>
     {
         var resp = wq.EndGetResponse(state);
         using (var sr = new StreamReader(
                               resp.GetResponseStream()))
         {
             Console.WriteLine("after:" + 
                 Thread.CurrentThread.ManagedThreadId);
             var content = sr.ReadToEnd();
             if (content.Contains("body"))
                 Console.WriteLine("found");
             else
                 Console.WriteLine("not found");
         }
     }, null);
}

before:9

after:14

found

一樣,看DoWork5。

public static void DoWork5_1()
{
    HttpClient client = new HttpClient();
    Console.WriteLine("before:" + 
                   Thread.CurrentThread.ManagedThreadId);
    client.GetStringAsync(
         "http://www.google.com").ContinueWith((result) =>
    {
        Console.WriteLine("after:" + 
                 Thread.CurrentThread.ManagedThreadId);
        if (result.Result.Contains("body"))
            Console.WriteLine("found");
        else
            Console.WriteLine("not found");
    });
}

before:9

after:11

found

還是一樣,所以除了DoWork2是阻塞寫法外,其他DoWork1、3、4、5都是兩條Thread,那麼它們都是一樣的嗎? 為了證明她們是不是一樣的,我們再寫另一組來測試,就概念上而言,非同步的寫法其實跟Thread Pool有很大關係,因為非同步多半會用到Thread,而.NET在處理非同步的時候會利用Thread Pool來最有效利用Thread。下面這個例子設定了最大的Thread Pool所能開出的最大Thread,並利用ThreadStatic來針對每個Thread標記(也就是說,在每個Thread中都有一個_tag變數,彼此不干擾)。

[ThreadStatic]
private static volatile int _tag;

public static void SetEnviorment()
{
    ThreadPool.SetMinThreads(50, 12);
    ThreadPool.SetMaxThreads(50, 12);
    for (int i = 0; i < 50; i++)
    {
         ThreadPool.QueueUserWorkItem((s) =>
         {
             _tag = -1;
             Thread.Sleep(2000);
         }, null);
    }
    Thread.Sleep(4000);
}

這裡我們設定Thread Pool最大的Threads數量是50條,後面的12指的是IO Completion Thread,這是一個很特別的概念,我們後面會詳細討論。再設好數量後,這裡利用迴圈為這50條Threads裡面的_tag變數設值,這個用途是後面使用async/await或是傳統非同步作業時,可以辨識該Thread是來自於Thread Pool或不是。完成後看DoWork1的第三版本。

public static async void DoWork1_2()
{
     HttpClient client = new HttpClient();
     Console.WriteLine("before:" + 
            Thread.CurrentThread.ManagedThreadId + "," + _tag);
     var content = await client.GetStringAsync("http://www.google.com");
     Console.WriteLine("after:" + 
            Thread.CurrentThread.ManagedThreadId + "," + _tag);
     if (content.Contains("body"))
         Console.WriteLine("found");
     else
         Console.WriteLine("not found");
}

注意,要呼叫前得先呼叫SetEnviorment來限制Thread Pool。

static void Main(string[] args)
{
    SetEnviorment();
    _tag = -3;
    DoWork1_2();
    Console.ReadLine();           
}

能猜到結果嗎?

before:9,-3

after:62,0

found

第一行的tag是-3,這是因為我們在主Thread設定tag為-3,接著await之後出現的數字很神奇,她是0,明顯不是來自Thread Pool,事實上,她是IO Completion Thread,不過後面再談。不過如果你以為await之後tag一定是0,那麼就錯了,看下面的這版。

public static async void DoWork1_3()
{
    HttpClient client = new HttpClient();
    Console.WriteLine("before:" + 
          Thread.CurrentThread.ManagedThreadId + "," + _tag);
    await Task.Delay(1000);
    Console.WriteLine("after:" + 
          Thread.CurrentThread.ManagedThreadId + "," + _tag);
}

before:9,-3

after:10,-1

視await後面呼叫的方法,await之後不一定都是來自Thread Pool,也不一定都是來自IO Completion Thread。

基本上DoWork2是阻塞,所以我們直接跳DoWork3第三版本。

public static void DoWork3_2()
{
    WebClient client = new WebClient();
    Console.WriteLine("before:" + 
            Thread.CurrentThread.ManagedThreadId + "," + _tag);
    client.DownloadStringCompleted += (s, args) =>
    {
        Console.WriteLine("after:" + 
               Thread.CurrentThread.ManagedThreadId + "," + _tag);
        var content = args.Result;
        if (content.Contains("body"))
            Console.WriteLine("found");
        else
            Console.WriteLine("not found");
    };
    client.DownloadStringAsync(new Uri("http://www.google.com"));
}

結果是啥呢?

before:10,-3

after:42,-1

found

所以,我們之前假設DoWork1 = DoWork3是錯的,兩者後面的Thread不同,一個是來自IO Completion Thread,一個是來自Thread Pool。

看DoWork4。

public static void DoWork4_2()
{
    WebRequest wq = WebRequest.Create("http://www.google.com");
    Console.WriteLine("before:" + 
            Thread.CurrentThread.ManagedThreadId + "," + _tag);
    wq.BeginGetResponse((state) =>
    {
        var resp = wq.EndGetResponse(state);
        using (var sr = new StreamReader(resp.GetResponseStream()))
        {
            Console.WriteLine("after:" + 
                Thread.CurrentThread.ManagedThreadId + "," + _tag);
            var content = sr.ReadToEnd();
            if (content.Contains("body"))
                Console.WriteLine("found");
            else
                Console.WriteLine("not found");
        }
    }, null);
}

before:9,-3

after:62,0

found

這與DoWork1大致相同,都是來自於IO Completion Thread,看DoWork5。

public static void DoWork5_2()
{
    HttpClient client = new HttpClient();
    Console.WriteLine("before:" + 
           Thread.CurrentThread.ManagedThreadId + "," + _tag);
    client.GetStringAsync(
            "http://www.google.com").ContinueWith((result) =>
    {
        Console.WriteLine("after:" + 
              Thread.CurrentThread.ManagedThreadId + "," + _tag);
        if (result.Result.Contains("body"))
            Console.WriteLine("found");
        else
            Console.WriteLine("not found");
    });
}

before:9,-3

after:10,-1

found

是不是開始覺得怪怪的了? 讓我們總結一下。

DoWork1以await為分界,兩條Thread,一條是Caller Thread,一條是IO Completion Thread。

DoWork3是兩條Thread,一條是Caller Thread,一條是Thread Pool Thread。

DoWork4是兩條Thread,一條是Caller Thread,一條是IO Completion Thread。

DoWork5是兩條Thread,一條是Caller Thread,一條是Thread Pool Thread。

所以,DoWork1 不等於DoWork3,也不等於DoWork5。

因此,我們可以分成兩種狀態,DoWork1、DoWork4脈絡是同樣的,DoWork3、5脈絡是一樣的。

那麼哪一種寫法正確? 這很難說,視乎情況,以Console來說,DoWork1大概是C# 5.0中最好的寫法,但如果換成Windows Form、WPF、ASP.NET就不一定,因為async/await和WebClient有個機制會捕捉Caller Context,這會使得DoWork1等於DoWork3(但不會等於DoWork5)。

public async void DoWork1()
{
    HttpClient client = new HttpClient();
    listBox1.Items.Add("before:" + Thread.CurrentThread.ManagedThreadId);
    var content = await client.GetStringAsync("http://www.google.com");
    listBox1.Items.Add("after:" + Thread.CurrentThread.ManagedThreadId);
    if (content.Contains("body"))
        Console.WriteLine("found");
    else
        Console.WriteLine("not found");
}       

public void DoWork3()
{
    WebClient client = new WebClient();
    listBox1.Items.Add("before:" + Thread.CurrentThread.ManagedThreadId);
    client.DownloadStringCompleted += (s, args) =>
    {
        listBox1.Items.Add("after:" + Thread.CurrentThread.ManagedThreadId);
        var content = args.Result;
        if (content.Contains("body"))
            Console.WriteLine("found");
        else
            Console.WriteLine("not found");
    };
    client.DownloadStringAsync(new Uri("http://www.google.com"));
}

這兩個函式在Windows Form、WPF還有ASP.NET的結果是一樣的,這是因為WebClient還有await在非同步之前會捕捉SynchronizationContext,完成後會嘗試Post到SynchronizationContext去執行,簡單的說,就是所謂的UI Thread。

就結果而言,在Windows Form/WPF/ASP.NET中,如果後續動作與UI無關的話,DoWork5才是最好的寫法,可以省下捕捉 SynchronizationContext的動作及帶來的後遺症,不過缺點就是要寫成像WebClient非同步那樣,很不好懂,幸運的是其實他有另一種寫法。

private async void button2_Click(object sender, EventArgs e)
{
    HttpClient client = new HttpClient();
    MessageBox.Show("Before : " + 
         Thread.CurrentThread.ManagedThreadId.ToString());
    var content = await client.GetStringAsync(
             "http://www.google.com").ConfigureAwait(false);
    MessageBox.Show("After : " + 
         Thread.CurrentThread.ManagedThreadId.ToString());
}

ConfigureAwait等於false的話,就不會捕捉SynchronizationContext了。

 

IO Completion Thread

  通常,看到這邊你應該有點頭昏了,但同時也浮出了一些關鍵疑問,第一個是IO Completion Thread,要了解這個東西,讓我們由DoWork1開始。

public static async void DoWork1()
{
    HttpClient client = new HttpClient();
    var content = await client.GetStringAsync("http://www.google.com");
    if (content.Contains("body"))
        Console.WriteLine("found");
    else
        Console.WriteLine("not found");
}

首先,GetStringAsync有沒有開Thread? 簡單的回答是沒有,在OS層級中有一個機制叫做IOCP,可以由下面的連結取得詳細介紹。

https://msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx

簡略的說,發送IO請求可以是同步,也可以是不同步,當需要使用不同步時,就會進入IOCP的流程,以這個例子來說,GetStringAsync發出IO請求並標記為非同步,並且在結構中放入一個tag,此時這個IO動作會立刻返回,這意味著不會有回傳值,因為IO根本還沒做,當IO做完後,OS會觸發一個中斷,此時取得的結構包含了tag與回傳值,接著交給IO Completion Handling Thread,這裡的IO Completion Handling Thread會取出回傳值然後依據tag取出callback,開出IO Completion Thread來執行,也就是await 後面所在的Thread,用一張圖來解釋。

這裡要注意,IO Completion Thread Handling一開始就存在了,她是一個Thread,一個IO Completion Thread Handling可以處理上萬個IO Completion Event,所以別誤為她是GetStringAsync開出來的Thread,而且他也不是Thread Pool所管轄的。

使用IOCP的目的很簡單,就是榨取Call IO後等待IO回傳的時間,一般如果是同步,發出IO就會進入等待IO回傳,但如果是非同步,這裡不會有任何Thread等待回傳,不要以為IO Completion Thread Handling是,因為一個IO Completion Thread Handling可以服務上萬個IO請求,所以不能認為GetStringAsync會對應一個IO Completion Thread Handling,是一個IO Completion Thread Handling可以對應上萬個GetStringAsync。

所以,我們可以這樣說,當await之後的動作是IO動作,那麼不會有多餘的Thread等待IO動作結束,當IO動作結束後,有一個IO Completion Thread會開出來(IO Completion Thread通常在另一個Thread Pool)。但如果await之後的不是IO動作(例如Task.Delay),那麼就會有一個Thread在等待其回傳。

當處於WPF/Windows From/ASP.NET等有SynchronizationContext情況下,IO Completion Thread 會直接把callback Post到SynchronizationContext的Thread,但對我們而言,她是隱形的。

事實上,WebClient、HttpClient內部都是WebRequest,所以一定會有IO Completion Thread產生,不同的寫法會使得IO Completion Thread後續的行為不同。

 

SynchronizationContext

  async/await的出生有很大一部分是為了解決當非同步動作發生後,前後兩個Thread不同而帶來的困擾,通常在UI就會觸發不能在Main Thread以外的Thread存取UI控制項的例外。

所以如果把ConfigureAwait(false)拿掉,就不會出現這個例外,這是因為await會捕捉SynchronizationContext,在完成後把callback Post到同樣的Thread來執行,等同下面這樣。

public async void DoWork1_1()
{
    HttpClient client = new HttpClient();
    listBox1.Items.Add("before:" + 
        Thread.CurrentThread.ManagedThreadId);
    var context = SynchronizationContext.Current;
    var content = await client.GetStringAsync(
             "http://www.google.com").ConfigureAwait(false);
    context.Post((state) =>
    {
       listBox1.Items.Add("after:" + 
              Thread.CurrentThread.ManagedThreadId);
       if (content.Contains("body"))
           Console.WriteLine("found");
       else
           Console.WriteLine("not found");
    }, null);
}

但這也帶來困擾,因為不一定每次的await都會存取UI,所以盡可能在不用存取UI時使用ConfigureAwait(false),這可以讓你的程式不會因為要回到Main Thread而排隊或是形成阻塞,另一個這樣做的好處是你可以自行控制存取UI的區段,如果依賴await,那麼await後的動作全部都會在Main Thread中排隊,但如果你使用ConfigureAwait(false),就可以將存取UI那塊自己排入Post,得到最大的非同步。當然,如果Caller 沒擁有SynchronizationContext,那這些都不需要考慮,直接await不加ConfigureAwait即可。

那麼怎麼樣可以形成阻塞?看下面的例子。

private async Task<string> GetWebResult2()
{
    HttpClient client = new HttpClient();
    var content = 
       await client.GetStringAsync("http://www.google.com");
    if (content.Contains("body"))
        return "html found";
    else
        return "html not found";
}
       

private void button1_Click(object sender, EventArgs e)
{
    var content = GetWebResult2().Result;
    MessageBox.Show(content.Length.ToString());
}

這其實不是正常的寫法,通常在使用await的時候,我們不應該直接取用Result這個屬性值,這意味著你馬上要取得回傳值,所以Result的get函式會等待直到回傳值到達,以這個例子來說,await之後嘗試將callback排入SynchronizationContext的Thread,但是取Result的get函式阻塞在SynchronizationContext的Thread,所以死結就發生了。

你有兩個選擇,一是使用ConfigureAwait(false),不嘗試回到SynchronizationContext的Thread,後遺症是你不能直接存取UI控制項,另一個是把Caller也標記為async,並一併使用await,如下。

private async void button3_Click(object sender, EventArgs e)
{
     var content = await GetWebResult2();
     MessageBox.Show(content.Length.ToString());
}

其實很簡單,就是不要直接取Result,除非你知道你在做什麼。

 

async/await的擴展

  呃,終於來到這裡了,接下來來談談async/await的擴展手法,以下面這個例子來說。

private async void button1_Click(object sender, EventArgs e)
{
    HttpClient client = new HttpClient();
    var content = await client.GetStringAsync("http://www.google.com");
    if (content.Contains("body"))
        button1.Text = "found";
    else
        button1.Text = "not found";
}

會被擴展為下面這樣。

private void button1s_Click(object sender, EventArgs e)
{
     d__1 stateMachine = new d__1()
     {
        __this = this,
        sender = sender,
        e = e,
        t__builder = AsyncVoidMethodBuilder.Create(),
        _1__state = -1
     };
     stateMachine.t__builder.Start<d__1> (ref stateMachine);
}

sealed class d__1 : IAsyncStateMachine
{
     public int _1__state;
     public Form1 __this;
     private string s__3;
     public AsyncVoidMethodBuilder t__builder;
     private TaskAwaiter<string> u__1;
     private HttpClient client_5__1;
     private string content5__2;
     public EventArgs e;
     public object sender;

     public void MoveNext()
     {
         int num = this._1__state;
         try
         {
            TaskAwaiter<string> awaiter;
            if (num != 0)
            {
               this.client_5__1 = new HttpClient();
               awaiter = this.client_5__1.GetStringAsync("http://www.google.com").GetAwaiter();
               if (!awaiter.IsCompleted)
               {
                  this._1__state = num = 0;
                  this.u__1 = awaiter;
                  Form1.d__1 stateMachine = this;
                  this.t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Form1.d__1>(
                              ref awaiter, ref stateMachine);
                  return;
               }
            }
            else
            {
               awaiter = this.u__1;
               this.u__1 = new TaskAwaiter<string>();
               this._1__state = num = -1;
            }
            string result = awaiter.GetResult();
            awaiter = new TaskAwaiter<string>();
            this.s__3 = result;
            this.content5__2 = this.s__3;
            this.s__3 = null;
            if (this.content5__2.Contains("body"))
            {
                this.__this.button1.Text = "found";
            }
            else
            {
                this.__this.button1.Text = "not found";
            }
         }
         catch (Exception exception)
         {
            this._1__state = -2;
            this.t__builder.SetException(exception);
            return;
         }
         this._1__state = -2;
         this.t__builder.SetResult();
    }

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

我把擴展後的語法修正,所以這是可以編譯的程式碼,你可以逐步的追蹤來尋其脈絡,下圖是大概的流程。

更精確點,要補上Capture Context部分。

如果要更精確的模擬其行為,下面的程式碼趨近於真實行為。

public sealed class d__1simulate : IAsyncStateMachine
{
     public int _1__state;
     public Form1 __this;
     private WebRequest client5__1;
     private SynchronizationContext context;
     public AsyncVoidMethodBuilder t__builder;
     private string content5__2;
     public EventArgs e;
     public object sender;

     public void MoveNext()
     {
         int num = this._1__state;
         try
         {
             if (num != 0)
             {
                context = SynchronizationContext.Current;
                this.client5__1 = WebRequest.Create("http://www.google.com");
                this.client5__1.BeginGetResponse((state) =>
                {
                   var resp = this.client5__1.EndGetResponse(state);
                   using (var sr = new StreamReader(resp.GetResponseStream()))
                   {
                      this.content5__2 = sr.ReadToEnd();
                   }
                   if (context != null)
                       context.Post((s) =>
                       {
                           MoveNext();
                       }, null);
                   else //simulate io completion thread
                       Task.Run(() =>
                       {
                             MoveNext();
                       });
                }, null);
                if (true) //simulate, actually is check Awaiter is complete or not.
                {
                   this._1__state = num = 0;
                   return;
                }
             }
             else
             {
                this._1__state = num = -1;
             }
             if (this.content5__2.Contains("body"))
             {
                 this.__this.button2.Text = "found";
             }
             else
             {
                 this.__this.button2.Text = "not found";
             }
          }
          catch (Exception exception)
          {
             this._1__state = -2;
             return;
          }
          this._1__state = -2;
       }

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


private void button2_Click_1(object sender, EventArgs e)
{
    d__1simulate d = new d__1simulate()
    {
        __this = this,
        sender = sender,
        e = e,
        _1__state = -1
    };
    d.MoveNext();
}

我把HttpClient移除,改用WebRequest,也就是把HttpClient一併模擬了,連同AVoidMethodBuilder也一起模擬,這更能貼近於真實行為,不過請注意我用Task.Run來處理沒有Context的狀態,這實際上是IO Completion Thread,但在.NET裡,我們無法建立出IO Completion Thread。

 

再來一次

 

  現在再回頭來看看一開始的例子,看看能不能一眼看出Thread的脈絡。

public static async void DoWork1()
{
      HttpClient client = new HttpClient();
      var content = await client.GetStringAsync("http://www.google.com");
      if (content.Contains("body"))
          Console.WriteLine("found");
      else
          Console.WriteLine("not found");
}

這裡有一個IO Completion Thread會產生,在沒有SynchronizationContext的情況下(Console),await後面的程式碼是執行在IO Completion Thread 中,在有SynchronizationContext情況下(WPF/WinForm/ASP.NET)會有一個IO Completion Thread產生,隨即把執行權交給UI Thread。

public static void DoWork3()
{
      WebClient client = new WebClient();
      client.DownloadStringCompleted += (s, args)=>
      {
          var content = args.Result;
          if (content.Contains("body"))
              Console.WriteLine("found");
          else
              Console.WriteLine("not found");
      };
      client.DownloadStringAsync(new Uri("http://www.google.com"));
}

這個例子在沒有SynchronizationContext的情況下(Console),會有一個IO Completion Thread產生,但是他隨即會把執行權交給Thread Pool所產生的Thread,在有SynchronizationContext情況下(WPF/WinForm/ASP.NET)會有一個IO Completion Thread產生,隨即把執行權交給UI Thread。

public static void DoWork4()
{
     WebRequest wq = WebRequest.Create("http://www.google.com");
     wq.BeginGetResponse((state) =>
     {
        var resp = wq.EndGetResponse(state);
        using (var sr = new StreamReader(resp.GetResponseStream()))
        {
            var content = sr.ReadToEnd();
            if (content.Contains("body"))
                Console.WriteLine("found");
            else
                Console.WriteLine("not found");
        }
     }, null);
}

這個例子在會有一個IO Completion Thread產生,程式碼是執行在IO Completion Thread中。

public static void DoWork5()
{
        HttpClient client = new HttpClient();            
        client.GetStringAsync("http://www.google.com").ContinueWith((result) =>
        {
            if (result.Result.Contains("body"))
                Console.WriteLine("found");
            else
                Console.WriteLine("not found");
        });
}

這個例子會有一個IO Completion Thread產生,但是他隨即會把執行權交給Thread Pool所產生的Thread。

public static async void DoWork1_3()
{
        HttpClient client = new HttpClient();
        Console.WriteLine("before:" + Thread.CurrentThread.ManagedThreadId + "," + _tag);
        await Task.Delay(1000);
        Console.WriteLine("after:" + Thread.CurrentThread.ManagedThreadId + "," + _tag);
}

這個例子在沒有SynchronizationContext情況下,會產生一個Thread Pool Thread,執行Delay動作,完成後產生一個Thread Pool Thread來執行await後面的程式碼,在有SynchronizationContext情況下,await區段後面的程式碼會執行在UI Thread中。

 

async but no await

  當你把一個Method標示為async時,不管有沒有使用await,編譯器都會擴展這個Method,以下面這個例子來說。

static async void Test()
{
}

會擴展成這樣。

[AsyncStateMachine(typeof(<Test>d__0)), DebuggerStepThrough]
private static void Test()
{
    <Test>d__0 stateMachine = new <Test>d__0();
    stateMachine.<>t__builder = AsyncVoidMethodBuilder.Create();
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start<<Test>d__0>(ref stateMachine);
}

簡單的說,編譯器會擴展async Method,然後逐一尋找裡面的await區段來處理。

還有一種很詭異的寫法。

static async Task<int> Test()
{
    return 15;
}

擴展後是下面這樣。

………………….
private void MoveNext()
{
    int num2;
    int num = this.<>1__state;
    try
    {
        num2 = 15;
    }
    catch (Exception exception)
    {
        this.<>1__state = -2;
        this.<>t__builder.SetException(exception);
        return;
    }
    this.<>1__state = -2;
    this.<>t__builder.SetResult(num2);
}
………………..
[AsyncStateMachine(typeof(<Test>d__0)), DebuggerStepThrough]
private static Task<int> Test()
{
    <Test>d__0 stateMachine = new <Test>d__0();
    stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start<<Test>d__0>(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

只要在Test中沒有任何的await,也沒有呼叫任何會產生Thread的函式,不管怎麼寫,呼叫Test都不會有任何的Thread產生,這基本上就是一段無意義的寫法,所以將一個函式標示為async不代表會有Thread產生,呼叫await也不代表會有Thread產生,端看整個脈絡而定。

 

所以呢?

async/await在WPF/WinForm/ASP.NET與Console的行為會有些微不同,主要是SynchronizationContext的捕捉與否。

async/await在處理非同步IO時,不會等待IO的回傳,IO Completion Handling不算是等待,因為一個IO Completion Handling可以等待上萬個非同步IO。

async/await在處理非IO動作時,視其後續的的呼叫函式而定,正常情況下至少會有一個Thread,以Task.Delay來說,內部是一個System.Threading.Timer,在這個Thread結束時會透過ThreadPool執行指定的動作,也就是await後面的那段,這個脈絡跟你使用ContinueWith是一樣的(Task.Delay開出一個來自ThreadPool的Thread,結束後開出另一個ThreadPool的Thread來執行await區段或是ContinueWith指定的動作)。

async/await在處理IO動作時,沒有SynchronizationContext下,IO完成後的是一個IO Completion Thread,嚴格上來說不是我們可以控制的Thread,不來自Thread Pool。

async/await在處理IO 動作時,沒有SynchronizationContext下,同時所能處理的量依據IO Completion Thread數量而定,預設是1000,而Thread Pool則視乎OS而定及.NET 版本而定,在我的電腦,.NET Framework 4.6是1023。

這意味著當數量超過時就會排隊,下面這個例子可以模擬IO Completion Thread排隊的狀態。

[ThreadStatic]
private static volatile int _tag;

public static void SetEnviorment()
{
    ThreadPool.SetMinThreads(50, 12);
    ThreadPool.SetMaxThreads(50, 12);
    for (int i = 0; i < 50; i++)
    {
         ThreadPool.QueueUserWorkItem((s) =>
         {
             _tag = -1;
             Thread.Sleep(2000);
         }, null);
    }
    Thread.Sleep(4000);
}

private static int _count = 0;
public static async void DoWork6()
{
    HttpClient client = new HttpClient();
    var content = await client.GetStringAsync("http://www.google.com");
    Console.WriteLine(_count);
    Interlocked.Increment(ref _count);
    Thread.Sleep(TimeSpan.FromMinutes(1));
}

public static void DoWork6_Call()
{
    for (int i = 0; i < 24; i++)
         DoWork6();
}

static void Main(string[] args)
{
    int max, iomx;
    SetEnviorment();
    DoWork6_Call();
    Console.ReadLine();           
}

另外,只要回傳是Task<T>,就可以用await,但Task不等於Thread。

最後,async可以用在delegate,例如下面這樣。

Task.Run(async()=>
{
    HttpClient client = new HttpClient();
    var content = await client.GetStringAsync("http://www.google.com");
});

另外,在5.0時 catch區段不支援async/await,這些應該大家早就知道了。

 

到底怎麼用?

  如果不是WinForm/WPF/ASP.NET這種有SynchronizationContext的環境下,運用await不需考慮太多,是非同步IO時後面會由IO Completion Thread接手,非IO動作會由Thread Pool接手。

  如果是WinForm/WPF/ASP.NET,建議多思考使用ConfigureAwait(false)的可能性,也就是最小化排入UI Thread的部分,當然,真的也不需要過度憂慮,畢竟這本來就是設計成方便UI類型寫法的,如果都不用也很浪費,除非真的很需要由這個地方榨出效能。