Open棟梁Project - マイクロソフト系技術情報 Wiki
目次 †
概要 †
awaitの意味は、スレッドを留めずにCallbackを待つということらしい。
async/awaitはそういう動きをする、非同期プログラムを容易に記述できる。
- async/awaitの登場で、マルチスレッド処理として実装しなくても、
非同期処理を同期型処理と、ほぼ変わらない記述で実装可能になった。
- しかし、その実態はとても複雑、詳しくはコチラを参照。
- 内部がどう動いているか詳しく把握せんとシステムは安定しない、
- 中身をシッカリ理解して使用している人が少ないので注意。
非同期処理の実装の歴史 †
Thread、ThreadPool? †
従来の、マルチスレッド・プログラミング。
- スレッドの並列実行はOSが裏で無意識にしてくれていた。
- タイムスライスで細切れ/ラウンドロビンで論理的に並列実行。
- CPUのコア数に応じて、物理的に並列実行。
- しかし、以下の処理は意識的に実装する必要があった。
- 非同期処理をスレッド関数として分離して実装する。
- スレッド関数を作成したワーカースレッドに渡す。
- スレッド関数の結果をメインスレッドで待ち合わせる。
APM、EAP (Control.Invoke) †
「Windowsメッセージングキュー(Control.Invoke)」による方式。
Open棟梁の「非同期呼出フレームワーク」がこの方式で実装されている。
TAP (async/await) †
async/awaitは、TAP(Task-based Asynchronous Pattern)の方式で実装されている。
用途 †
- 主に、非同期処理(≠並列処理)を実装するために導入された。
- UIスレッドからのサーバ呼出のハングアップを防止するために使用される。
- 内部的には、UIスレッドから、時間のかかるバックグラウンド処理
(ネットワーク バインドまたは I/O バインドの処理)を分離する。
- この仕組みは、Webサーバのスレッド枯渇を防ぐ非同期Controllerなどにも応用されている。
使い方 †
- asyncで修飾したTaskを返す非同期メソッドから、awaitステートメントを付与して呼び出す。
- 若しくは、Task.Run()で実行して、Task.Wait()で待ち合わせる。
タスク分割方法 †
await(Task.Run)を切れ目として、
プログラマが意識して時間のかかるバックグラウンド処理
(ネットワーク バインドまたは I/O バインドの処理)を分割する。
- await
- await前の処理はフォアグラウンドで実行される。
- awaitで呼び出す非同期メソッドはバックグラウンドで実行される。
- await後の処理は、コールバックとして実装せずにフォアグラウンドに復帰する。
- Task.Run ~ Task.Wait()
- Task.Run前の処理はフォアグラウンドで実行される。
- Task.Run()で呼び出す非同期メソッドはバックグラウンドで実行される。
- Task.Wait()後の処理は、コールバックとして実装せずにフォアグラウンドに復帰する。
仕組み †
- 非同期化は、同期コンテキスト(Windowsメッセージングキュー、スレッド)などを使用して行われる。
- 並列実行処理を実装するための基盤ではない
- Fire & ForgetやTask.WhenAll?で並列実行処理を実装できるが、
await、Task.Wait()の待ち合わせまでがセットになっている仕組みなので、
そもそも、並列実行処理を実装するための基盤ではないことに注意する。
- 並列実行処理を実装する場合は、Threadや、ThreadPool?を使用するようにする。
使い方 †
- async/awaitの登場で、同期型処理と、ほぼ変わらない記述で実装可能になった。
- しかし、デバッグの時は非同期で実行されていることを意識する必要がある。
非同期メソッドの戻り値 †
void型 †
Fire & Forgetの場合、戻り値は不要。
Task型 †
Awaitableの場合、
Task<T>型 †
Awaitableの場合、
Task.FromResult? †
Task.FromResult?を使用して、完了状態のTaskを生成することができる。
詳細については、下記を参照。
非同期メソッドの呼び出し †
Task.Run()、Task.Wait()メソッド †
- Task.Run()
- 非同期メソッドを実行してTask、Task<TResult>を返す。
- Task.Wait()
- Task、Task<TResult>の実行が完了するまで待機する。
Task.WhenAll?() †
- Task.WhenAll?()メソッドで複数のタスクを待機するタスクを取得する。
- このTaskをTask.Wait()するとTask毎の例外をAggregateException?型として取得可能。
await演算子 †
- Task の実行が完了するまで待機する。
- await 非同期メソッド()
- await Task.Run()
- await演算子の使い方
- 非同期メソッドを呼び出すときに、await演算子(後述)を利用する。
- メソッド内でawait演算子(後述)を利用する場合、async修飾子でメソッドを修飾する。
- await演算子は、async修飾子の付くメソッドの中で1つ以上記述できる。
詳細 †
ここでは、
- 「非同期メソッドの種類」と
- 「同期コンテキスト」の
組み合わせによって、await後の処理が、
どのように実行されるのかについて説明する。
非同期メソッドの種類 †
Fire & Forget †
- 非同期呼び出しで投げっぱなす場合。
呼び出し元が非同期メソッドの完了を待機する必要がない場合
(この場合、非同期メソッドでもreturn文を書く必要が無い)。
- 具体的には、GUIアプリケーションのイベント・ハンドラに適用する場合。
(それ以外の用途での利用はハマる原因になるらしいので非推奨らしい)
Awaitable †
- 非同期呼び出しの呼び出し元でTask.Wait()で待ち合せする場合。
呼び出し元が非同期メソッドの完了を待機する必要がある場合
(この場合、非同期メソッドは、Task、Task<T>をリターンする)。
- 上記のGUIアプリケーションのイベント・ハンドラ以外に適用する。
同期コンテキスト †
実行環境によって持つ同期コンテキストの種類が異なる。
GUIアプリケーション †
GUIアプリケーションでの同期コンテキストは
「Windowsメッセージングキュー(Control.Invoke)」になる。
- Fire & Forget
GUIアプリケーションのawaitの次の処理は、
同期コンテキストのControl.Invokeによって、
UIスレッド上でシーケンシャルに実行される。
Consoleアプリケーション †
Consoleアプリケーションでの同期コンテキストは「null」になる。
- Awaitable
- Consoleアプリケーション内のawaitの次の処理が実行されるスレッドは一意ではなくなる。
- ただし、処理自体は、シーケンシャルに実行される。
ThreadPool? †
Task.Runを使用した場合の同期コンテキストは、
マルチスレッド環境下の「ThreadPool?」になる。
- Awaitable
- Task.Run内のawaitの次の処理が実行されるスレッドは一意ではなくなり、
- 且つ、Task.Run内のawaitの次の処理は、UIスレッドに戻らなくなる。
- ただし、処理自体は、シーケンシャルに実行される。
ASP.NET †
ASP.NETアプリケーションでの同期コンテキストは
マルチスレッド環境下の「System.Threading.SynchronizationContext?.Current」になる。
- Awaitable
- ASP.NETアプリケーションのawaitの次の処理が実行されるスレッドは一意ではなくなる。
- ただし、処理自体は、シーケンシャルに実行される。
- 最終的に結果は、リクエストを受け付けたスレッドにバインドされる。
詳しくは、非同期Controllerを参考にする。
この辺の
スタック・トレースを見ると、
- 「SynchronizationContext?」と
- 「AsyncControllerActioninvoker?」は
同じ事らしいと解る。
並列実行 †
- GUI以外の同期コンテキストでFire & Forgetを実行した場合(非推奨)や、
- Task.WhenAll?で複数のTaskをTask.Wait()した場合(ThreadPool?で実行される)では、
並列実行を始めるので、
特に、Consoleアプリケーションの同期コンテキスト下では、
新規に同期処理の実装が必要になることがある。
スレッド同期 †
async/awaitは非同期呼び出しで投げっぱなす場合に利用されるため、
スレッド同期をあまり考慮していないが、スレッド同期を行う場合は以下に注意する。
仕組みから †
スレッドを使用した処理を記述しないが、分割されたタスクは、
- 同一スレッドで動作することも
- 別スレッドで動作することもある。
このため、一連の処理が(分割されたタスクが)、
- 異なるスレッドで実装される保証は無い。
- 同じスレッドで実行される保証も無い。
スレッド同期ツールキットは使用不可 †
従って、スレッド同期のlock等は無意味。
従来のスレッド同期ツールキットは使用不可。
lock/mutex/semaphoreはtaskで全て使用禁止 †
WaitFor?[Single|Multi]Objectは例外的に使用可 †
これは、Win32の待機関数のWaitFor?[Single|Multi]Objectは、
スレッドに限らず様々な「オブジェクト」の状態が変化する(シグナル状態になる)のを待つ関数であるためと考える。
その他 †
進捗報告 †
上記の仕組みで動いているとすると、進捗報告をどう実装するかが?であるが、
以下を見ると、Progressクラス、IProgress<T>インターフェースを使用するらしい事が解る。
詳しい仕組みは不明だが、GUI上で動作しているため、
同期コンテキストのControl.Invokeを使用しているものと思われる。
ContinueWith? †
- 複数のタスクを継続に連結し、時間のかかる同期コンテキストを経由しない。
ConfigureAwait? †
- 同期コンテキストを使用するか・しないかを制御できる。
ガイドライン †
ハマりどころがホントに多いらしいのでガイドライン的なモノを整備したい。
参考 †
参考 †