読者です 読者をやめる 読者になる 読者になる

飽きっぽい人のブログ@qwerty2501

プログラマとしてもテスターとしても中途半端な人のブログ

async/await ~非同期なライブラリは楽じゃない~

※個人的な備忘録的なものです。 こっちとかこっちのが良くまとめられています。
ライブラリ制作者向けの内容になっているのでアプリ製作者にはあまり関係がないかもしれません なお、サンプルコードは全てWindowsストアアプリとして実行したものとします

デッドロックで泣きを見ないように

下のようなライブラリのコードがあるとします

gist11215254

このライブラリのDoAsyncは呼び出され方によってはデッドロックされてしまいます
下のコードがその例になります

gist11215568

原因はTaskのWaitメソッドでロックしたスレッドに対して、HeavyWorkAsyncメソッドでワーカースレッドで作業していたTaskが元のUIスレッドに戻ろうとしたためです
図にすると以下のようになります

f:id:qwerty2501:20140424220821p:plain

Waitするなと思った方がいらっしゃるかもしれませんが、使用するのは自分ではなく他人のアプリ製作者(もしくは自分のライブラリを使用してさらにライブラリを制作しようとしている人)になりますので、そのような命令をすることはできません
この問題を解決するにはDoAsyncの実装を下のようにします

gist11215506

こんどはHeavyWorkAsync().ConfigureAwait(false)の戻り値をawaitしていますね Taskを直接awaitするとConfigureAwait(true)を規定で呼び出すらしいです
引数のbool値についてですが、trueを渡すと呼び出した時点のスレッドの同期コンテキストを使用してもとのスレッドにTaskの処理を戻そうとします。falseを渡すと処理が行われたスレッドでそのまま続行します
実装をHeavyWorkAsync().ConfigureAwait(false)に変更した場合の図は以下のようになります

f:id:qwerty2501:20140424224909p:plain

そもそもこのような一行の呼び出しではawaitする必要はないかと思います
ただし、非同期処理の例外をキャッチしたい場合はConfigureAwait(false)したものをawaitする必要があります

全てConfigureAwait(false)すれば良いのか

そこが非同期プログラミングの難しいところで、非同期プログラミングにおいては「大体これでいいけど例外が存在する」というのが多いようです
taskオブジェクトをConfigureAwait(false)するのもそれに倣っており、ライブラリ内の非同期実装は大体ConfigureAwait(false)で取得した待機オブジェクトをawaitすればよいのですが、ユーザに対するイベント通知を行う場合には注意する必要があります
下のようなコードについて説明します

gist11217368 このクラスのSomeEventHandlerの通知によってライブラリユーザがUIの更新をすることが高いと予想されるケースです。 この様なケースではSomeEventHandler呼び出し時にUIスレッドに戻っていることが望ましいでしょう
DoAsync内ではHeavyWorkAsync()の呼び出しをConfigureAwait(false)で取得した待機オブジェクトでawaitしています
その後、SomeAsyncは規定のawaitで待機しています
この場合、SomeAsyncでは既定のawaitを行っているのでUIスレッドに戻りそうに見えますが、実際にはSomeEventHandlerを呼び出す時点ではUIスレッドとは別のスレッドになっています。これは事前に呼び出したHeavyWorkAsyncのタスクをConfigureAwait(false)した時点でUIスレッドには戻らないためです
そのため、SomeAsyncのタスクを規定のawaitで待機してしまうのは無駄にスレッドをロックしてしまうことになります。

SomeEventHandlerをUIスレッドで呼び出すには下記のようにします

gist11217686 awaitする前に同期コンテキストをキャッシュしておいて、SomeEventHandlerの呼び出しを同期コンテキストで切り替えてあげればSomeEventHandlerをUIスレッド上で実行することが可能です

しかし、このような対策を行ってもライブラリユーザ側で下のような使われ方をすると意味がありません

gist11218041 DoAsync呼び出し前に別のタスクをConfigureAwait(false)したものをawaitで待機された場合、DoAsyncの呼び出し時点ですでにUIスレッドではないため、DoAsync内で同期コンテキストを使って動作スレッドの切り替えを行っても無意味となってしまいます
この様にイベント通知をUIスレッド上で呼び出すのは難しいです。また、同期コンテキストによるスレッドの切り替えはかなり重いコストがかかるので自分のライブラリが同期コンテキストによるスレッドの切り替えを必要としているかよく吟味する必要があります(特にサーバーでも使われるライブラリの場合は不要な場合が多い)

まとめ

・基本的にはライブラリ内でawaitする場合はConfigureAwait(false)すること
・ConfigureAwait(false)すると元のスレッド(主にUIスレッド)には戻らないので、必要であれば同期コンテキストによる動作スレッドの切り替えを行うこと
・同期コンテキストによる動作スレッドの切り替えの要否はそのライブラリのコンセプト及び設計に基づいて決めること

非同期ってやっぱり難しいですね
うまく付き合っていくしかないかなあとは思いますが、ほかの言語だとどうなんでしょう
楽なのかな