Async/await は非同期プログラミングを簡略化しますが、特定の間違いが繰り返し現れます。 この記事では、非同期コードで最も一般的な 5 つのバグについて説明し、各バグを修正する方法について説明します。
非同期メソッドが同期的に実行される
メソッドに 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 メソッドを待機できません
同期 void戻りメソッドを非同期に変換する場合は、戻り値の型を Task に変更します。 戻り値の型を void のままにすると、メソッドは "async 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 メソッドには、次の欠点があります。
- 例外は監視されません。 非同期 void メソッドでスローされた例外は、メソッドの開始時にアクティブだった SynchronizationContext に反映されます。 呼び出し元は、これらの例外をキャッチできません。
- 呼び出し元は処理の完了を追跡できません。
Taskがないと、操作がいつ終了するかを知るメカニズムはありません。 - テストは困難です。 テストでメソッドを待機して動作を確認することはできません。
非同期コードによるブロックが原因のデッドロック
このバグは、"完了しない" 非同期コードの最も一般的な原因です。これは、シングル スレッド Waitを持つスレッドで同期的にブロック (Task<TResult>.Result、GetAwaiter、またはGetResult.SynchronizationContext) するときに発生します。
デッドロックを引き起こすシーケンス:
- 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
デッドロックを回避する方法
次の 1 つ以上の戦略を使用します。
ブロックしないでください。
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 ModuleConfigureAwait(false)を使用すると、ランタイムは、継続を元のSynchronizationContextにマーシャリングしないように指示します。 この方法では、ブロックする呼び出し元を保護し、不要なスレッド ホップを回避することでパフォーマンスを向上させます。
Warnung
静的コンストラクターのデッドロック。 CLR は、静的コンストラクター (cctor) の実行中にロックを保持します。 静的コンストラクターがタスクでブロックされ、そのタスクの継続で同じ型 (またはコンストラクション チェーンに関係する型) でコードを実行する必要がある場合、 cctor ロックが保持されているため、継続を続行できません。 静的コンストラクター内の呼び出しを完全にブロックしないようにします。
タスク <タスク> のラップ解除
StartNewなどのメソッドに非同期ラムダを渡すと、返されるオブジェクトは単純なTask<Task>ではなく、Task<Task<TResult>> (またはTask) になります。 非同期ラムダが最初のイールド 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
この問題は、次の 3 つの方法のいずれかで解決します。
Run を代わりに使用します。
Task.RunTask<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 Module2 回待機 します (最初に外側のタスク、次に内部タスク)。
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
タスクを返す呼び出しで await が見つからない
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 を行います。
こちらも参照ください
.NET