轉自:http://www.cnblogs.com/xishuai/p/asp-net-sync-over-async.htmlhtml
async/await 是咱們在 ASP.NET 應用程序中,寫異步代碼最經常使用的兩個關鍵字,使用它倆,咱們不須要考慮太多背後的東西,好比異步的原理等等,若是你的 ASP.NET 應用程序是異步到底的,包含數據庫訪問異步、網絡訪問異步、服務調用異步等等,那麼恭喜你,你的應用程序是沒問題的,但有一種狀況是,你的應用程序代碼比較老,是同步的,但如今你須要調用異步代碼,這該怎麼辦呢?有人可能會說,很簡單啊,不是有個 .Result 嗎?但事實真的就這麼簡單嗎?咱們來探究下。git
首先,放出幾篇經典文章:github
上面文章的內容,咱們後面會說。光看不練假把式,因此,若是真正要體會 sync over async,咱們還須要本身動手進行測試:數據庫
先說明一下,在測試代碼中,異步調用使用的是 HttpClient.GetAsync 方法,而且測試請求執行兩次,關於具體的分析,後面再進行說明。編程
測試代碼:網絡
[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; } }
輸出結果:app
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,爲何請求線程只有一個?後面再說,咱們接着測試。異步
測試代碼:async
[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 始終是同樣的。
測試代碼:
[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
的時候,線程就阻塞了。
測試代碼:
[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
中究竟是什麼鬼?
測試代碼:
[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 都沒有輸出的。
測試代碼:
[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,結果卻徹底不一樣。
測試代碼:
[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
簡單總結:注意這是異步的寫法,調用和被調用方法都是異步的,從輸出的結果中,咱們就會發現,這種狀況和上面的六種狀況,有一個最明顯的區別就是,請求線程和結束線程不是同一個,說明什麼呢?線程是異步等待的。
先梳理一下測試結果:
上面這麼多的測試狀況,看起來可能有些暈,咱們先從最簡單的第二種狀況開始分析下,首先,頁面是同步方法,請求線程能夠看做是一個主線程 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(異步中同步)是否可行?經過上面的測試結果能夠得出是可行的,但要注意一些寫法問題:
固然最好的方式是異步到底。