await を使用して非同期 APIを実装する際に使用する重要なことの 1 つは、API impl 内でタスクを待機する場合は常にConfigureAwait(false)を使用することです。これにより、TaskAwaiter の既定の動作 (現在の同期コンテキスト) ではなく、TPL の既定の動作 (スレッドプール) を使用して TPL が await の再開をスケジュールできるようになります。
現在の同期コンテキストを使用することは、コンシューマーにとって適切な既定の動作です。既に UI スレッドを使用している場合に、UI スレッドに戻るのを待機するなどのことが可能になるためです。ただし、メソッドの残りの部分を実行するために UI スレッドを使用できない場合、UI スレッドに戻ろうとすると問題が発生する可能性があります。メソッドを実行するスレッドを取得する方法await
は、内部でデリゲートを作成する標準の .NET 規則です。次に、これらのデリゲートは、何らかのディスパッチ メカニズム (WinForms メッセージ ポンプ、WPF ディスパッチャなど) で処理されるように送信されます。
ただし、同じコンテキストに戻ろうとすることは、通常、API 実装にとって間違ったことです。これは、実行に使用できる元のコンテキストに暗黙的に依存するためです。
たとえば、UI スレッドにコードがあるとします。
void MyUIThreadCode() {
Task asyncTask = MyAsyncMethod();
asyncTask.Wait();
}
async Task MyAsyncMethod() {
await DownloadSomethingAsync();
ComputeSomethingElse();
}
この種のコードは[b]非常に[/b]書きたくなるし、非常に簡単にハングアップします。典型的なケースは、 の内部MyAsyncMethod()
に、デフォルトの同期コンテキスト スケジューリングを使用する await があることです。つまり、UI コンテキストでは DownloadSomethingAsync() メソッドが呼び出され、ダウンロードが開始されます。
MyAsyncMethod()
await
次に、オペランドが「完了」しているかどうかをテストします。ダウンロードが完了していないとしましょう。したがって、定義された動作await
は、メソッドの「残り」を切り離し、await
オペランドが実際に完了したら実行するようにスケジュールすることです。
したがって、メソッドの残りの部分を実行するための状態は、デリゲートに隠され、MyAsyncMethod()
独自のタスクを に戻しますMyUIThreadCode()
。
返されたタスクをMyUIThreadCode()
呼び出すようになりました。Task.Wait()
しかし、問題はTask
、.NET では、実際には「完了」の概念を持つあらゆるものの汎用表現であるということです。オブジェクトがあるからといって、Task
それがどのように実行されるか、どのように完了するかを保証するものは何もありません。ご想像のとおり、保証されていないもう 1 つのことは、暗黙的な依存関係です。
したがって、上記の例でMyAsyncMethod()
は、Task でデフォルトの await 動作を使用し、現在のコンテキストでメソッドの継続をスケジュールします。MyAsyncMethod()
の返されたタスクが完了したと見なされる前に、メソッドの継続を実行する必要があります。
ただし、MyUIThreadCode()
呼び出さWait()
れます。定義された動作は、現在のスレッドをブロックし、現在の関数をスタックに保持し、タスクが完了するまで効果的に待機することです。
ユーザーがここで気付いていなかったのは、ブロックされているタスクがアクティブに処理している UI スレッドに依存Wait()
しているということです。呼び出しでブロックされている関数の実行でまだビジーであるため、UI スレッドはそれを行うことができません。
- MyUIThreadCode() がメソッド呼び出しを終了するには、
Wait()
(2) を返す必要があります。
- Wait() が戻るには、asyncTask が完了する必要があります (3)。
- asyncTask を完了するには、メソッドの継続を
MyAsyncMethod()
実行する必要があります (4)。
- メソッドの継続を実行するには、メッセージ ループを処理する必要があります (5)。
- メッセージ ループが処理を続行するには、MyUIThreadCode() が (1) を返す必要があります。
循環依存関係が綴られており、最終的にどの条件も満たされず、実質的に UI スレッドがハングします。
これを ConfigureAwait(false) で修正する方法は次のとおりです。
void MyUIThreadCode() {
Task asyncTask = MyAsyncMethod();
asyncTask.Wait();
}
async Task MyAsyncMethod() {
await DownloadSomethingAsync().ConfigureAwait(false);
ComputeSomethingElse();
}
ここで何が起こるかというと、メソッドの継続はMyAsyncMethod()
、現在の同期コンテキストではなく、TPL のデフォルト (スレッドプール) を使用するということです。そして、その動作を伴う現在の条件は次のとおりです。
- MyUIThreadCode() がメソッド呼び出しを終了するには、
Wait()
(2) を返す必要があります。
- Wait() が戻るには、asyncTask が完了する必要があります (3)。
- asyncTask を完了するには、メソッドの継続を
MyAsyncMethod()
実行する必要があります (4)。
- (NEW)メソッドの継続を実行するには、スレッド プールが処理されている必要があります (5)。
- 実際、スレッドプールは常に処理中です。実際、.NET スレッドプールは、高いスケーラビリティ (動的にスレッドを割り当てて破棄する) と低レイテンシ (要求が処理を開始する前に古くなることを許容する最大しきい値がある) を実現するように非常に調整されています。スループットを維持するための新しいスレッド)。
あなたは .NET がすでに堅実なプラットフォームであることに賭けています。.NET ではスレッド プールを非常に真剣に考えています。
それで、おそらく問題はWait()
呼び出しにあると思うかもしれません...なぜ彼らは最初にブロッキング待機を使用したのですか?
答えは、本当に必要な場合があるということです。たとえば、Main() メソッドの .NET 契約では、Main() メソッドが戻るとプログラムが終了します。または... つまり、プログラムが完了するまでMain() メソッドがブロックされます。
インターフェイス コントラクトや仮想メソッド コントラクトなどの他のものには、通常、そのメソッドが戻る前に特定のことが実行されるという特定の約束があります。インターフェイスまたは仮想メソッドが Task を返さない限り... 呼び出された非同期 API に対して何らかのブロッキングを行う必要がある可能性があります。これは、その 1 つの状況での非同期の目的を効果的に無効にします...しかし、おそらく、別のコードパスでの非同期の恩恵を受けるでしょう。
したがって、非同期タスクを返す API プロバイダーの場合、ConfigureAwait(false) を使用することで、返されたタスクに予期しない暗黙の依存関係がないことを確認できます (たとえば、UI メッセージ ループがまだアクティブにポンピングされているなど)。依存関係を含めることができればできるほど、より優れた API になります。
お役に立てれば!