ASP.NET sync over async(異步中同步,什麽鬼?)
轉自:http://www.cnblogs.com/xishuai/p/asp-net-sync-over-async.html
async/await 是我們在 ASP.NET 應用程序中,寫異步代碼最常用的兩個關鍵字,使用它倆,我們不需要考慮太多背後的東西,比如異步的原理等等,如果你的 ASP.NET 應用程序是異步到底的,包含數據庫訪問異步、網絡訪問異步、服務調用異步等等,那麽恭喜你,你的應用程序是沒問題的,但有一種情況是,你的應用程序代碼比較老,是同步的,但現在你需要調用異步代碼,這該怎麽辦呢?有人可能會說,很簡單啊,不是有個 .Result 嗎?但事實真的就這麽簡單嗎?我們來探究下。
首先,放出幾篇經典文章:
- async & await 的前世今生
- 異步編程 In .NET(中文資料中,寫異步最棒的兩篇文章)
- HttpClient.GetAsync(…) never returns when using await/async
- Don‘t Block on Async Code(下面測試中的第三種情況)
- Should I expose synchronous wrappers for asynchronous methods?(sync over async 透徹)
上面文章的內容,我們後面會說。光看不練假把式,所以,如果真正要體會 sync over async,我們還需要自己動手進行測試:
- 1. 異步調用使用 .Result,同步調用使用 .Result
- 2. 異步調用使用 await,同步調用使用 Task.Run
- 3. 異步調用使用 await,同步調用使用 .Result
- 4. 異步調用使用 Task.Run,同步調用使用 .Result
- 5. 異步調用使用 await .ConfigureAwait(true),同步調用使用 .Result
- 6. 異步調用使用 await .ConfigureAwait(false),同步調用使用 .Result
- 7. 異步調用使用 await,異步調用使用 await
- 8. 測試總結
先說明一下,在測試代碼中,異步調用使用的是 HttpClient.GetAsync 方法,並且測試請求執行兩次,關於具體的分析,後面再進行說明。
1. 異步調用使用 .Result,同步調用使用 .Result
測試代碼:
[Route("")] [HttpGet] public string Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = Test(); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static string Test() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); using (var client = new HttpClient()) { var response = client.GetAsync(url).Result; System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return response.Content.ReadAsStringAsync().Result; } }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:13 Thread.CurrentThread.ManagedThreadId2:13 Thread.CurrentThread.ManagedThreadId3:13 Thread.CurrentThread.ManagedThreadId4:13 Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:6 Thread.CurrentThread.ManagedThreadId3:6 Thread.CurrentThread.ManagedThreadId4:6
簡單總結:同步代碼中調用異步,上面的測試代碼應該是我們最常寫的,為什麽沒有出現線程阻塞,頁面卡死的情況呢?而且代碼中調用了 GetAsync,為什麽請求線程只有一個?後面再說,我們接著測試。
2. 異步調用使用 await,同步調用使用 Task.Run
測試代碼:
[Route("")] [HttpGet] public string Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = Task.Run(() => Test2()).Result; System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static async Task<string> Test2() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); using (var client = new HttpClient()) { var response = await client.GetAsync(url); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return await response.Content.ReadAsStringAsync(); } }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:7 Thread.CurrentThread.ManagedThreadId3:11 Thread.CurrentThread.ManagedThreadId4:6 Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:7 Thread.CurrentThread.ManagedThreadId3:12 Thread.CurrentThread.ManagedThreadId4:6
簡單總結:根據上面的輸出結果,我們發現,在一個請求過程中,總共會出現三個線程,一個是開始的請求線程,接著是 Task.Run 創建的一個線程,然後是異步方法中 await 等待的執行線程,需要註意的是,ManagedThreadId1 和 ManagedThreadId4 始終是一樣的。
3. 異步調用使用 await,同步調用使用 .Result
測試代碼:
[Route("")] [HttpGet] public string Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = Test3().Result; System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static async Task<string> Test3() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); using (var client = new HttpClient()) { var response = await client.GetAsync(url); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return await response.Content.ReadAsStringAsync(); } }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:5 Thread.CurrentThread.ManagedThreadId2:5
簡單總結:首先,頁面是卡死狀態,ManagedThreadId3 並沒有輸出,也就是執行到 await client.GetAsync
的時候,線程就阻塞了。
4. 異步調用使用 Task.Run,同步調用使用 .Result
測試代碼:
[Route("")] [HttpGet] public string Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = Test4().Result; System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static async Task<string> Test4() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); return await Task.Run(() => { Thread.Sleep(1000); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return "xishuai"; }); }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:6 Thread.CurrentThread.ManagedThreadId3:7
簡單總結:和第三種情況一樣,頁面也是卡死狀態,但不同的是,ManagedThreadId3 是輸出的,測試它的主要目的是和第三種情況形成對比,以便了解 HttpClient.GetAsync
中到底是什麽鬼?
5. 異步調用使用 await .ConfigureAwait(true),同步調用使用 .Result
測試代碼:
[Route("")] [HttpGet] public string Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = Test5().Result; System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static async Task<string> Test5() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); using (var client = new HttpClient()) { var task = client.GetAsync(url); var response = await task.ConfigureAwait(true); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return await response.Content.ReadAsStringAsync(); } }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:6
簡單總結:和上面兩種情況一樣,頁面也是卡死狀態,它的效果和第三種完全一樣,ManagedThreadId3 都沒有輸出的。
6. 異步調用使用 await .ConfigureAwait(false),同步調用使用 .Result
測試代碼:
[Route("")] [HttpGet] public string Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = Test6().Result; System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static async Task<string> Test6() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); using (var client = new HttpClient()) { var task = client.GetAsync(url); var response = await task.ConfigureAwait(false); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return await response.Content.ReadAsStringAsync(); } }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:6 Thread.CurrentThread.ManagedThreadId3:10 Thread.CurrentThread.ManagedThreadId4:6 Thread.CurrentThread.ManagedThreadId1:8 Thread.CurrentThread.ManagedThreadId2:8 Thread.CurrentThread.ManagedThreadId3:11 Thread.CurrentThread.ManagedThreadId4:8
簡單總結:和第五種情況形成對比,僅僅只是把 ConfigureAwait 參數設置為 false,結果卻完全不同。
7. 異步調用使用 await,異步調用使用 await
測試代碼:
[Route("")] [HttpGet] public async Task<string> Index() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId1:" + Thread.CurrentThread.ManagedThreadId); var result = await Test7(); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId4:" + Thread.CurrentThread.ManagedThreadId); return result; } public static async Task<string> Test7() { System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId2:" + Thread.CurrentThread.ManagedThreadId); using (var client = new HttpClient()) { var response = await client.GetAsync(url); System.Diagnostics.Debug.WriteLine("Thread.CurrentThread.ManagedThreadId3:" + Thread.CurrentThread.ManagedThreadId); return await response.Content.ReadAsStringAsync(); } }
輸出結果:
Thread.CurrentThread.ManagedThreadId1:6 Thread.CurrentThread.ManagedThreadId2:6 Thread.CurrentThread.ManagedThreadId3:12 Thread.CurrentThread.ManagedThreadId4:12 Thread.CurrentThread.ManagedThreadId1:7 Thread.CurrentThread.ManagedThreadId2:7 Thread.CurrentThread.ManagedThreadId3:8 Thread.CurrentThread.ManagedThreadId4:8
簡單總結:註意這是異步的寫法,調用和被調用方法都是異步的,從輸出的結果中,我們就會發現,這種情況和上面的六種情況,有一個最明顯的區別就是,請求線程和結束線程不是同一個,說明什麽呢?線程是異步等待的。
8. 測試總結
先梳理一下測試結果:
- 異步調用使用 .Result,同步調用使用 .Result:通過,始終一個線程。
- 異步調用使用 await,同步調用使用 Task.Run:通過,三個線程,請求開始和結束為相同線程。
- 異步調用使用 await,同步調用使用 .Result:卡死,線程阻塞。
- 異步調用使用 Task.Run,同步調用使用 .Result:卡死,線程阻塞。
- 異步調用使用 await .ConfigureAwait(true),同步調用使用 .Result:卡死,線程阻塞。
- 異步調用使用 await .ConfigureAwait(false),同步調用使用 .Result:通過,兩個線程,await 執行為單獨一個線程。
- 異步調用使用 await,異步調用使用 await:通過,兩個線程,請求開始和結束為不同線程。
上面這麽多的測試情況,看起來可能有些暈,我們先從最簡單的第二種情況開始分析下,首先,頁面是同步方法,請求線程可以看作是一個主線程 1,然後通過 Task.Run 創建線程 2,讓它去執行 Test2 方法,需要註意的是,這時候主線程 1 並不會往下執行(從輸出結果可以看出),它會等待線程 2 執行,主要是等待線程 2 執行返回結果,在 Test2 方法中,一切是異步方法,await client.GetAsync 會創建又一個線程 3 去執行,並且線程 2 等待它返回結果,然後最終回到線程 1 上,在整個過程中,雖然有三個線程,但這三個線程並不是同時工作的,而是一個執行之後等待另一個執行的結果,所以整個執行過程還是同步的。
第三種和第二種情況的不同就是,異步調用由 Task.Run 改成了 .Result,然後就造成了頁面卡死,在 Don‘t Block on Async Code 這篇文章中,就是詳細說明的這種情況,為什麽會卡死呢?其實你從同樣卡死的第四種情況和第五種情況中,可以發現一些線索,ConfigureAwait 的說明是:試圖繼續回奪取的原始上下文,則為 true;否則為 false。什麽意思呢?就是它可以變身為請求線程,最能體現出這一點的是,如果設置為 true,那麽在這個線程中,就可以訪問 HttpContext.Current
,那為什麽在同步調用中,設置為 true 就造成頁面卡死呢?我們分析一下,頁面是同步方法,請求線程可以看作是一個主線程 1,然後調用 Test3 異步方法,這時候主線程 1,會在這裏等待異步的執行結果,在 Test3 方法中創建一個線程 2,因為把 ConfigureAwait 設置為了 true,那麽線程 2 就想把自己變身成為請求線程(謀權篡位),也就是線程 1,但是人家線程 1 現在正在門口等它呢?線程 2 卻想占有線程 1 的地位,很顯然,這是不成功的,那什麽情況下可以謀權篡位成功呢?就是線程 1 不在,也就是線程 1 回到線程池中了,這就是異步等待的效果,也是它的威力。
針對第三種情況,簡單畫了一個示意圖:
在第五種情況中,因為把 ConfigureAwait 設置為 false,線程 2 不想謀權篡位了,它只想老老實實的做事,把執行結果返回給請求線程 1,那麽整個請求執行過程就是順利的。
同步調用異步測試中,還剩一個第一種情況,它和其他情況不同的是,沒有異步方法,只是使用的是 .Result,那為什麽它是通過的?並且線程始終是一個呢?首先,頁面請求開始,創建一個請求線程 1,因為 Test 方法並不是異步方法,所以還是線程 1 去執行它,執行到了 client.GetAsync
這一步,因為沒有使用 await,所以並不會創建一個線程去執行它,並且最終的是,雖然 GetAsync 是異步方法,但再其實現代碼中,設置了 ConfigureAwait(false):
async Task<HttpResponseMessage> SendAsyncWorker(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) { using (var lcts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) { lcts.CancelAfter(timeout); var task = base.SendAsync(request, lcts.Token); if (task == null) throw new InvalidOperationException("Handler failed to return a value"); var response = await task.ConfigureAwait(false);//重點 if (response == null) throw new InvalidOperationException("Handler failed to return a response"); // // Read the content when default HttpCompletionOption.ResponseContentRead is set // if (response.Content != null && (completionOption & HttpCompletionOption.ResponseHeadersRead) == 0) { await response.Content.LoadIntoBufferAsync(MaxResponseContentBufferSize).ConfigureAwait(false); } return response; } }
所以,整個過程應該是這樣的,在測試代碼中始終是一個請求線程在執行,並且在 client.GetAsync
的執行中,會創建另外一個線程 2 去執行,然後線程 1 等待線程 2 的執行結果,因為 GetAsync 的實現並不在測試代碼中,所以表現出來就是一個線程在執行,雖然是異步方法,但它和同步方法一樣,為什麽?因為線程始終在等待另一個線程的執行結果,也就是說,在某一時刻,始終是一個線程在執行,其余線程都在等待。
sync over async(異步中同步)是否可行?通過上面的測試結果可以得出是可行的,但要註意一些寫法問題:
- 異步調用使用 .Result,而不能出現 await。
- 不能出現 ConfigureAwait(true)。
- 可以使用 Task.Run,但僅限於不返回結果的執行線程。
當然最好的方式是異步到底。
ASP.NET sync over async(異步中同步,什麽鬼?)