對 ASP.NET 非同步程式設計的一點理解

田園裡的蟋蟀發表於2015-10-31

本來這篇博文想探討下非同步中的異常操作,但自己在做非同步測試的時候,又對 ASP.NET 非同步有了新的認識,可以說自己之前對非同步的理解還是有些問題,先列一下這篇博文的三個解惑點:

  • async await 到底是什麼鬼???
  • 非同步操作中發生異常,該如何處理?
  • 非同步操作中發生異常(有無 catch throw 情況),Application_Error 會不會捕獲?

之前測試過非同步中的同步(很多種情況),這次我們把測試程式碼寫更復雜些(非同步中再進行非同步),程式碼如下:

[Route("")]
[HttpGet]
public async Task<string> Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = await Test();
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId6:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync("http://stackoverflow.com/questions/14996529/why-is-my-async-asp-net-web-api-controller-blocking-the-main-thread");
        await Test2();
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId5:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

public static async Task<string> Test2()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
    using (var client = new HttpClient())
    {
        var response = await client.GetAsync("http://stackoverflow.com/questions/33408905/pgadminiii-bug-on-query-tool");
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId);
        return await response.Content.ReadAsStringAsync();
    }
}

輸出結果(執行四次):

Thread.CurrentThread.ManagedThreadId1:8
Thread.CurrentThread.ManagedThreadId2:8
Thread.CurrentThread.ManagedThreadId3:6
Thread.CurrentThread.ManagedThreadId4:6
Thread.CurrentThread.ManagedThreadId5:6
Thread.CurrentThread.ManagedThreadId6:6

Thread.CurrentThread.ManagedThreadId1:7
Thread.CurrentThread.ManagedThreadId2:7
Thread.CurrentThread.ManagedThreadId3:8
Thread.CurrentThread.ManagedThreadId4:7
Thread.CurrentThread.ManagedThreadId5:7
Thread.CurrentThread.ManagedThreadId6:7

Thread.CurrentThread.ManagedThreadId1:5
Thread.CurrentThread.ManagedThreadId2:5
Thread.CurrentThread.ManagedThreadId3:5
Thread.CurrentThread.ManagedThreadId4:6
Thread.CurrentThread.ManagedThreadId5:6
Thread.CurrentThread.ManagedThreadId6:6

Thread.CurrentThread.ManagedThreadId1:8
Thread.CurrentThread.ManagedThreadId2:8
Thread.CurrentThread.ManagedThreadId3:8
Thread.CurrentThread.ManagedThreadId4:8
Thread.CurrentThread.ManagedThreadId5:8
Thread.CurrentThread.ManagedThreadId6:8

這個測試方法,我執行了無數次,大致就是上面的四種情況,我當時看到輸出結果,其實是很凌亂的,我也大家也一樣,並心裡有一些疑問:你這真是非同步程式設計嗎?為啥執行緒千奇百怪?並且最後那個還只有一個執行緒,這和同步有啥區別???

針對上面這個疑問,我想了很久,並對自己產生了一些質疑的聲音:你每天都在寫 async await 程式碼,你真的瞭解它嗎???然後我又重新找到上面那篇 jesse liu 的博文,反覆讀了很多篇,最後終於有了一些“頓悟”,結合上面的測試程式碼,我大致畫了一張示意圖:

結合上面的圖,我說一下自己的理解,在做測試的時候,HttpClient.GetAsync 儘量讓它執行時間長些,比如請求的 URL 可以是 stackoverflow 或 github(原因你懂得!),因為有個時間差,這樣我們可以更好的瞭解執行緒的執行情況,上面圖中“執行緒1、執行緒1x、執行緒3x、執行緒4x”等等,這些並不是不同執行緒,也就是說執行緒1有可能等於執行緒1x或執行緒3x。。。從上面的輸出結果就可以看出,用執行緒x來表示兩個輸出之間所經歷的 await 次數,這就證明了一個疑惑:await 並不一定會建立和之前不一樣的執行緒。

到底什麼是非同步???我個人覺得,async 非同步是一個偽概念,await 等待才是精髓,一個執行緒可以響應多個請求,如果是同步程式設計,一個執行緒在處理某一個請求的時候阻塞了(比如上面測試程式碼中的 HttpClient.GetAsync 網路操作),那麼這個執行緒就會一直等待它處理,在這個等待的過程中,那麼其他請求就不能再使用這個執行緒,又因為 IIS 執行緒池中的執行緒數量有限,那麼同步程式設計下,高併發將是一個頭疼的問題,試想一下,如果執行緒池中的執行緒數量為 100 個,這 100 個執行緒在同時處理 100 個請求的時候,都悲催的阻塞掉了,這時候第 101 個請求將無法執行,那麼併發量就是 100。

接上面,同樣的處理過程,如果是非同步程式設計,那將是什麼情況呢?比如一個執行緒在處理某一個請求的時候,執行到 await 操作,那麼這個執行緒將會釋放回到執行緒池,然後進行等待,等待的過程中,原來的那個執行緒就可以處理其他請求或者這個請求的其他操作,注意等待並不是執行緒等待,而是操作等待,我原來就很不理解這個地方,如果是執行緒等待,就表示這個執行緒會一直等待它完成,那和同步程式設計就是一樣的了,所以這種理解是錯誤的,你可以這樣理解:await 等待的過程中,沒有執行緒!!!

再接上面,等待操作完成之後,這時候就會從執行緒池中隨機拿一個執行緒繼續執行,拿到的這個執行緒有可能是 await 操作剛剛釋放掉的,但也有可能是其他執行緒,上圖中的 2-6 操作就是這樣,一圖勝千言:

瞭解了整個過程之後,你才會明白 async await 到底是什麼鬼?以及它真正的用武之地是什麼?簡單總結幾點內容:

  • async 非同步網路處理作用最明顯(HttpClient 請求或資料庫連線):這個我們大家都很清楚,也很好理解,如果是其他操作,比如一個非同步方法中你做了很多費時的計算,那這個非同步將沒什麼效果,說白了和同步一樣,而對於網路操作,我們一般不做處理,發起請求之後等待它完成就行,所以這時候執行到這的執行緒,可以釋放並會到執行緒池中,網路操作執行完成之後,再從執行緒池中隨機拿一個執行緒繼續執行。
  • async 非同步並不是真正意義上的“非同步”:什麼意思呢?你仔細看下上面測試的輸出結果,會發現 ManagedThreadId1-6 是順序輸出的,而不是先輸出 ManagedThreadId4 再輸出 ManagedThreadID3,所以,非同步和同步的執行過程是一樣的,並且一個請求下,執行時間也是一樣的,上面的非同步測試其實某種意義上,是測試不出任何東西的(從測試結果就可以看出),非同步並不能減少你的執行時間,而是增加你的請求執行數量,這個東西說白了,其實就是併發量。
  • async 非同步的精髓是 await:這個之前已經提到了,準確來說,async 非同步的精髓是 await 時的執行緒回收與完成之後的執行緒切換,這個操作最大的價值是,避免執行緒的浪費等待,充分利用執行緒的執行,有點類似於地主不能容忍奴隸閒著做無意義的事,而是希望他們 24 小時不停工作一樣。

另外,在 ASP.NET 應用程式中,我們可以使用 Thread.CurrentThread 來訪問當前的執行執行緒,我之前想做這樣一個測試,讓當前執行執行緒 Sleep 一段時間,看看其他執行緒會不會執行,但 Thread.CurrentThread 並沒有 Sleep 方法,而必須這樣訪問 Thread.Sleep(int millisecondsTimeout),如果這樣執行這段程式碼,那麼當前執行緒將會 Sleep,但其他執行緒並不會在它 Sleep 的時候,而繼續執行,為什麼?因為 CPU 在同一時間段內只能執行一個執行緒。

瞭解了 async await 到底是什麼鬼後,博文一開始剩下的兩個有關非同步操作中的異常問題,現在理解起來就非常容易了:

  • 非同步操作中發生異常,該如何處理?:和同步一樣處理,同步中報錯,非同步也一樣報錯,有人可能有這樣的疑問,比如測試程式碼中的 Index Action,執行到 await Test 內部操作的時候,突然丟擲異常了,然後就想當然的認為,既然是非同步執行的 Test 方法,那 Index 應該不會影響吧?其實你執行之後就會發現,Index 頁面還是會丟擲異常的,所以異常和非同步沒半毛錢關係
  • 非同步操作中發生異常(有無 catch throw 情況),Application_Error 會不會捕獲?:無 catch,Application_Error 會捕獲;有 catch 無 throw,Application_Error 不會捕獲;有 catch 有 throw,Application_Error 會捕獲。

如果我們想讓某一個非同步方法,在執行丟擲異常的時候,而不影響其他非同步方法,那我們就 catch 而不 throw,比如我們的測試程式碼:

[Route("")]
[HttpGet]
public async Task<string> Index()
{
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId);
    var result = await Test();
    System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId6:" + Thread.CurrentThread.ManagedThreadId);
    return result;
}

public static async Task<string> Test()
{
    try
    {
        System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId);
        using (var client = new HttpClient())
        {
            var response = await client.GetAsync("http://stackoverflow.com/questions/33408905/pgadminiii-bug-on-query-tool");
            System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId);
            throw new Exception("test exception");//這裡出現了異常
            return await response.Content.ReadAsStringAsync();
        }
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine("異常資訊:" + ex.Message);
        return "";
        //throw ex;
    }
}

這樣的效果就是 Index 頁面不會報錯,並且也不會影響其他方法執行,現在發現當時疑惑這個問題的時候,還蠻白痴的,還是那句話,異常和非同步沒半毛錢關係,相同的問題,同步也是這樣進行處理的。

博文內容有點多,如果你不願花時間看,可以直接記住這段話:如果你的應用程式請求訪問很少(併發很小),非同步和同步將是一樣的效果,非同步化改造是毫無意義的,而如果你的應用程式請求訪問很多(併發很大),那麼效果顯而易見,如果使用非同步將會為你省掉幾臺伺服器的錢,但程式碼非同步化並不能使你的應用程式執行速度加快(指的是程式碼執行速度),垃圾程式碼還是垃圾程式碼,並不會有任何的改善,所以,寫好“好的程式碼”很重要!!!

相關文章