非同期処理とディスパッチャー
24日・25日とWDDに行ってたわけですが。
講演者の皆様、UIスレッドとディスパッチャーの話で苦労されてた印象。この辺りの仕組み、どうなんだろうなーとか、少し書いておこうかと。
UIスレッドに紐付いたクラス
まず前提。
UIスレッド
まず、GUIがらみのクラスは、単一スレッドからしかアクセスできないように作ってあります。スレッド安全に作ろうとするとパフォーマンスが出ないので、いっそのこと、UIスレッド以外からアクセスがあったら例外を出して止まるように作ってあります。
この、GUIコンポーネントと紐付いているスレッドがUIスレッドです。
ディスパッチャー
エンド ユーザーからの入力なんかを受け付けているのもこのUIスレッドで、UIスレッド上で時間がかかる処理をすると、UIがフリーズします。
なので、時間がかかる処理をするときは、一度別スレッドで処理して、結果をUIスレッドに戻すというフローが必要です。
WPFやSilverlightでは、それを担うのがディスパッチャー。たいていのUI要素は、DependencyObjectというクラスから派生していて、このクラスのDispatcherプロパティを介することで、UIスレッド上に制御を戻すことができます。
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
this.Dispatcher.BeginInvoke(new
Action(() =>
{
//UIスレッドで実行すべき処理
}));
});
同期コンテキスト
で、この手の「UIスレッドに制御を戻す仕組み」は、フレームワークによってやり方が異なります。
上記の通り、WPFやSilverlightではDispatcherを使いますが、例えばWindowsフォームの場合はControlクラスのInvokeメソッドを使います。
この差を吸収するためのものが同期コンテキストです。SynchronizationContextを使います。現在のコンテキストを、Current静的プロパティで取得して、それにPostすることで、所望のスレッドに制御を移します。
var syncContext = SynchronizationContext.Current;
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
syncContext.Post(state =>
{
//UIスレッドで実行すべき処理
}, null);
});
Taskクラスの場合、スケジューラーを使う方が自然かもしれないです。「同期コンテキストに制御を移すスケジューラー」というのも作れて、以下のように書きます。
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
//UIスレッドで実行すべき処理
}, TaskScheduler.FromCurrentSynchronizationContext());
正直、静的なもの(SynchronizationContext.Current)に依存して実行結果が変わるとか、割とダメな仕様なんですが、他に手があるかというと無理っぽく、悩ましいところ。
意識させたら負けかな
lockステートメントとか、このディスパッチャーやら同期コンテキストやらの仕組みとか、どこかに隠してしまって、ライブラリ利用者からは見えないようにした方がいいのではないかと。学習コストも高ければ、利用方法誤った時のヤバさも半端ないので。
ということで、さっきの、↓みたいなコードはいまいちかなぁと。
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
})
.ContinueWith(t =>
{
this.Dispatcher.BeginInvoke(new
Action(() =>
{
//UIスレッドで実行すべき処理
}));
});
async/await
で、C# 5.0のasync/awaitは同期コンテキストの仕組みを完全に内部に隠してしまったと。
↓のようなコードで、ContinueWithとSynchronizationContext.Current.Postの組み合わせ相当のコードが生成されます。
async
Task RunAsync()
{
var value = await
Task.Run(() =>
{
//時間がかかる処理を別スレッドで
return someValue;
});
//UIスレッドで実行すべき処理
}
一応、Taskクラス自身がこの仕組みを担っているのではなく、Awaitable/Awaiterというパターンの別の型を介していて、この辺りを自作すれば、同期コンテキストを使うかどうかとかも選択可能です。不要なコストになりかねず、必ずしも同期コンテキスト使いたくないので。
awaitですべて解決!とはいかず
awaitが解決するのは、「メソッド呼び出し→1つの値が返ってくる」というフローだけだったりします。
それ以外、例えばセンサーAPIとか、ローミングAPIとかがそうなんですが、常に非同期に、複数の値が飛んできます。ストリーム的なイベント発生。
これは、以下のように、相変わらずイベント ベースで書くことになります。
void InitHandlers()
{
Windows.Storage.ApplicationData.Current.DataChanged += DataChangeHandler;
}
void DataChangeHandler(Windows.Storage.ApplicationData appData, object o)
{
//データのリフレッシュ
}
WDDで多くの講演者の方が悩まされてたのはこちら。UIの更新は、Dispatcherを介さないと実行時エラーに。
EAP(Event-based Asynchronous Pattern)と同期コンテキスト
.NET 2.0の頃に流行った、イベント ベースの非同期パターンでは、実は、内部で同期コンテキストを介してUIスレッドに制御を戻してくれてたりします。
var wc = new
WebClient();
wc.DownloadDataCompleted += (sender, args) =>
{
var result = args.Result;
//ここはUIスレッドで実行されてくれる
};
wc.DownloadDataAsync(uri);
これは仕組み的には、WebClientクラスが内部で頑張ってるだけ。中でSynchronizationContext.Current.Postを呼んでいるはず。WebClientクラス以外で同じような動作になってる保証は一切なし。
しかも、逆に同期コンテキストを使いたくない場合には困ってしまうという難題付き。特に、ただでさえ、静的なものに依存して動作が変わるいまいちな仕様なわけで、それを避ける方法がないというのが結構嫌な感じ。
ということでWinRTでは…
イベント ストリーム系の非同期処理では明示的なDispatcher.Invoke呼び出しが必要となりました。
void InitHandlers()
{
Windows.Storage.ApplicationData.Current.DataChanged += DataChangeHandler;
}
void DataChangeHandler(Windows.Storage.ApplicationData appData, object o)
{
Dispatcher.Invoke(CoreDispatcherPriority.Normal, (sender, args) =>
{
// UIスレッドで実行すべき処理
}
}
というか、そもそもWinRTにはSynchronizationContextクラスがない模様。やっぱり、静的なものに依存して動作が変わるのがいまいちと判断されたんですかねぇ。
うーん、awaitの方がうまく内部に隠してしまってるだけに、こちらで見えているのが残念な…
この手のイベント ストリーム処理は、やっぱRx使うべきかなぁ。
まとめ
- ディスパッチャーとか同期コンテキストとか、利用者側に意識させたら負け
- await/asyncでは割とうまく隠してる
-
イベント ベースの非同期ではダメ…
- Rx使うべきかなぁ
WinRTのXAMLは、その通りですね。たとえば、スプラッシュスクリーンを拡張する場合に、Dismissイベントで本来のページへ繊維させますが、UIスレッドではないのでDispatcherが必須になります。Dispatcherを意識したくないのであれば、HTML/JSで作ってねという話になるんだけど…
JSでの非同期パターンになるとWinJS.Promiseオブジェクトになりますが、then もしくは done でイベントハンドラチェーンを書いていくパターンなので、勘違いすると破綻していきますね。
arai
2012年4月26日 at 10:38
JSの側は、単一UIフレームワークで、かつ、パフォーマンスがあまり問題にされないからできるんでしょうねぇ。
ufcpp
2012年4月26日 at 15:03