非同步/等待簡化了非同步程式設計,但某些錯誤會反覆出現。 本文將介紹非同步程式碼中最常見的五個錯誤,並示範如何修復每一個錯誤。
非同步方法以同步方式執行
把關鍵字加 async 到方法裡,並不會讓方法在背景執行緒上執行。 它告訴編譯器在方法主體中允許 , await 並將回傳值包裝成 Task。 當你呼叫非同步方法時,它會同步執行,直到到達未完成的等待檔中的第一個 await 方法。 若方法不包含await表達式,或其等待的每個等待對象都已完成,則該方法會在呼叫執行緒上完全結束:
public static class SyncExecutionExample
{
public static Task<int> ComputeAsync()
{
// No await in this method — it runs entirely synchronously.
return Task.FromResult(42);
}
}
Public Module SyncExecutionExample
Public Function ComputeAsync() As Task(Of Integer)
' No Await in this method — it runs entirely synchronously.
Return Task.FromResult(42)
End Function
End Module
此處方法會立即回傳已完成的任務,因為它從未讓步。 當非同步方法缺少 await 表達式時,編譯器會發出警告。
如果你的目標是將 CPU 限制的工作卸載到執行緒池執行緒,請用 Run 代替 async:
public static class OffloadExample
{
public static int ComputeIntensive()
{
int sum = 0;
for (int i = 0; i < 1_000; i++)
sum += i;
return sum;
}
public static Task<int> ComputeOnThreadPoolAsync()
{
return Task.Run(() => ComputeIntensive());
}
}
Public Module OffloadExample
Public Function ComputeIntensive() As Integer
Dim sum As Integer = 0
For i As Integer = 0 To 999
sum += i
Next
Return sum
End Function
Public Function ComputeOnThreadPoolAsync() As Task(Of Integer)
Return Task.Run(Function() ComputeIntensive())
End Function
End Module
欲了解何時使用 Task.Run的更多指引,請參見 同步方法的非同步包裝器。
不能等異步空洞方法
當你將同步 void的 -returning 方法轉換成非同步時,將 return 類型改為 Task。 如果你將返回類型保留為 void,該方法將變成「非同步空」,無法等待:
public static class AsyncVoidExample
{
// BAD: async void — can't be awaited.
public static async void DoWorkBadAsync()
{
await Task.Delay(100);
}
// GOOD: async Task — callers can await this.
public static async Task DoWorkGoodAsync()
{
await Task.Delay(100);
}
}
Public Module AsyncVoidExample
' BAD: Async Sub — can't be awaited.
Public Async Sub DoWorkBadAsync()
Await Task.Delay(100)
End Sub
' GOOD: Async Function returning Task — callers can await this.
Public Async Function DoWorkGoodAsync() As Task
Await Task.Delay(100)
End Function
End Module
非同步 void 方法有特定用途:UI 框架中的頂層事件處理程序。 在事件處理程序之外,非同步方法總是回傳Task或Task<T>。 非同步空法有以下缺點:
- 例外情況被忽視。 非同步 void 方法中拋出的例外會傳播到方法開始時仍然作用中的 SynchronizationContext。 來電者無法察覺這些例外情況。
- 來電者無法追蹤完成進度。 沒有
Task,就沒有機制知道操作何時結束。 - 測試很困難。 你不能等到測試方法來驗證它的行為。
非同步程式碼阻塞引起的死鎖
這個錯誤是導致非同步程式碼「永遠無法完成」最常見的原因。當你在單一執行緒環境中同步阻塞(呼叫 Wait、 Task<TResult>.Result、或 GetAwaiter.GetResult)時,就會發生這個問題。
導致死結的序列:
- UI 執行緒(或舊版 ASP.NET 中的 ASP.NET 請求執行緒)上的程式碼會呼叫非同步方法並封鎖回傳任務。
- 非同步方法等待一個未完成任務而不使用
ConfigureAwait(false)。 - 當所期待的任務完成時,後續動作會嘗試向原始
SynchronizationContext發送回傳。 - 該上下文的執行緒會被阻塞,等待任務完成——死結。
public static class DeadlockExample
{
public static async Task<string> GetDataAsync()
{
// Without ConfigureAwait(false), this continuation
// posts back to the original SynchronizationContext.
await Task.Delay(100);
return "data";
}
public static void CallerThatDeadlocks()
{
// On a single-threaded SynchronizationContext (e.g. UI thread),
// the following line deadlocks because the continuation needs
// the same thread that .Result is blocking.
string result = GetDataAsync().Result;
}
}
Public Module DeadlockExample
Public Async Function GetDataAsync() As Task(Of String)
' Without ConfigureAwait(False), this continuation
' posts back to the original SynchronizationContext.
Await Task.Delay(100)
Return "data"
End Function
Public Sub CallerThatDeadlocks()
' On a single-threaded SynchronizationContext (e.g. UI thread),
' the following line deadlocks because the continuation needs
' the same thread that .Result is blocking.
Dim result As String = GetDataAsync().Result
End Sub
End Module
如何避免僵局
請採用以下一項或多項策略:
不要封鎖。 用來
await代替.Result或.Wait():public static class DeadlockFix1 { public static async Task CallerFixedAsync() { // Use await instead of .Result string result = await DeadlockExample.GetDataAsync(); Console.WriteLine(result); } }Public Module DeadlockFix1 Public Async Function CallerFixedAsync() As Task ' Use Await instead of .Result Dim result As String = Await DeadlockExample.GetDataAsync() Console.WriteLine(result) End Function End Module在圖書館程式碼中使用
ConfigureAwait(false)。 當您的函式庫方法不需要在呼叫者的上下文中恢復時,請在每個ConfigureAwait(false)內指定await。public static class DeadlockFix2 { public static async Task<string> GetDataSafeAsync() { await Task.Delay(100).ConfigureAwait(false); return "data"; } }Public Module DeadlockFix2 Public Async Function GetDataSafeAsync() As Task(Of String) Await Task.Delay(100).ConfigureAwait(False) Return "data" End Function End Module使用
ConfigureAwait(false)會告訴執行階段不要將延續傳輸回原始SynchronizationContext。 這種方法保護了阻擋的呼叫者,並透過避免不必要的執行緒跳躍來提升效能。
警告
靜態建構器死結。 CLR 在執行靜態建構子cctor時會保留鎖。 如果靜態建構函式在任務中阻塞,而該任務的後續作業需要執行相同類型(或建構鏈中涉及的類型)的程式碼,則後續作業無法進行,因為 cctor 鎖被佔用。 完全避免在靜態建構器中封鎖呼叫。
任務<任務> 解包
當你將一個非同步 lambda 傳給像 StartNew這樣的方法時,回傳的物件是 Task<Task> (或 Task<Task<TResult>>),而非簡單的 Task。 外部任務在非同步 lambda 首次遇到其第一個產生點 await 時就完成。 它不會等內在任務完成:
public static class TaskTaskBugExample
{
public static async Task DemoAsync()
{
var sw = Stopwatch.StartNew();
// StartNew returns Task<Task>, not Task.
// The outer task completes immediately when the lambda yields.
await Task.Factory.StartNew(async () =>
{
await Task.Delay(1000);
});
// Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s");
}
}
Public Module TaskTaskBugExample
Public Async Function DemoAsync() As Task
Dim sw = Stopwatch.StartNew()
' StartNew returns Task(Of Task), not Task.
' The outer task completes immediately when the lambda yields.
Await Task.Factory.StartNew(Async Function()
Await Task.Delay(1000)
End Function)
' Elapsed shows ~0 seconds, not ~1 second.
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s")
End Function
End Module
解決這個問題的方法有三種:
請改用 Run。
Task.Run自動解包Task<Task>public static class TaskTaskFix1 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Run(async () => { await Task.Delay(1000); }); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix1 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Run(Async Function() Await Task.Delay(1000) End Function) Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End Module請看 Unwrap 結果:
public static class TaskTaskFix2 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); await Task.Factory.StartNew(async () => { await Task.Delay(1000); }).Unwrap(); Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix2 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Await Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function).Unwrap() Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End Module等待兩次 (先是外部任務,再是內部任務):
public static class TaskTaskFix3 { public static async Task DemoAsync() { var sw = Stopwatch.StartNew(); Task<Task> outerTask = Task.Factory.StartNew(async () => { await Task.Delay(1000); }); Task innerTask = await outerTask; await innerTask; Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s"); } }Public Module TaskTaskFix3 Public Async Function DemoAsync() As Task Dim sw = Stopwatch.StartNew() Dim outerTask As Task(Of Task) = Task.Factory.StartNew(Async Function() Await Task.Delay(1000) End Function) Dim innerTask As Task = Await outerTask Await innerTask Console.WriteLine($"Elapsed: {sw.Elapsed.TotalSeconds:F2}s") End Function End Module
缺席等待任務回傳電話
如果你在某個方法中呼叫任務回傳方法 async 但未等待它,該方法會開始非同步操作,但不會等待它完成。 編譯器會發出警告,指出 C# 中的 CS4014 和 Visual Basic 中的 BC42358 的情形:
public static class MissingAwaitExample
{
// BAD: Task.Delay is started but never awaited.
public static async Task PauseOneSecondBuggyAsync()
{
Task.Delay(1000); // CS4014 warning
}
// GOOD: await the task.
public static async Task PauseOneSecondAsync()
{
await Task.Delay(1000);
}
}
Public Module MissingAwaitExample
' BAD: Task.Delay is started but never awaited.
Public Async Function PauseOneSecondBuggyAsync() As Task
Task.Delay(1000) ' Warning BC42358
End Function
' GOOD: Await the task.
Public Async Function PauseOneSecondAsync() As Task
Await Task.Delay(1000)
End Function
End Module
將結果儲存在變數中可以抑制警告,但無法修復根本的錯誤。 除非你刻意想要「開火後就忘」的行為,否則永遠是 await 任務。