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,但僅限於不返回結果的執行線程。
當然最好的方式是異步到底。