非同期呼び出しの名前
いや、マジで知らなかったというか、「ビギンエンドのやつ」とか「シンコンのやつ」で通じますって(笑
というわけで、岩永さんの 非同期.pptx at Silverlight を囲む会 in 東京 #2 でさらさらっと書かれていることを、ちょっとだけ補足。
APM (Asynchronous Programming Model)
.NET 1.0 時代からあるビギンエンドの奴、名前のまんま Logic を呼び出す際に BeginLogic() と呼び出して EndLogic() で待機する。
// 同期呼び出し TResult Logic<T1, T2, TResult>(T1 param1, T2 param2) // APM 呼び出し IAsyncResult BeginLogic(T1 param1, T2 param2, Action<IAsyncResult> callback, object state); TResult EndLogic(IAsyncResult asyncResult);
BeginLogic() には Logic() にないパラメータが2つ増える。callback で渡したデリゲートは非同期処理として裏で稼働していた Logic が完了した場合に呼び出されるコールバック関数。state は、Win32 API 等でお馴染みのコールバックに任意の情報を持たせるのが面倒だった時代の名残で、APM は state に対して何もしない。興味がなければ null を渡しても設定してもいいし、任意の何かを渡しておいてもよい。state に与えたパラメータは IAsyncResult の AsyncState プロパティに保存される。
Logic の実行結果を取得するには、EndLogic() を呼び出す。EndLogic() を呼び出した時点で、まだ Logic が完了していなければ、完了するまで待つことになる。また、Logic が例外によって異常終了していた場合、通常は EndLogic() を呼び出すとその例外が re-throw される。
BeginLogic の戻り値はどこかに保存しなくてもコールバックで引数として渡されるので捨ててもよいのですが、ポルナレフのようなセリフを吐くために CompletedSynchronously プロパティを調べてから捨ててください。
なお、.NET では Delegate 型(MulticastDelegate 型)に BeginInvoke() というメソッドが用意されているため、安全性や機能性は別として、すべてのメソッドが APM によって非同期呼び出し可能になっています。
EAP (Event-based Asynchronous Pattern)
シンコンの奴。シンコンとは SynchronizationContext のことで、.NET 2.0 で導入されました。APM との違いは結果の通知先として Logic の所属するコンテキストに対応したディスパッチャが必須になったことで、非同期処理の呼び出し元コンテキストへ結果を返せるようになったことです。…わかりにくいですね^^ 具体的な言い方をすると、EAP の利点は「UI スレッドから呼び出したら、UI スレッドに戻ってくる」ということです。つまり、APM では
{ // Logic を呼び出し、結果が出たら TextBox へ表示する BeginLogic(..., DisplayResult, this.TextBox); } private void DisplayResult(IAsyncResult ar) { // state として渡しておいた TextBox を取得 var displayTo = ar.AsyncState as TextBox; // 結果を表示する ---> NG displayTo.Text = EndLogic(ar); /* * // こんなかんじで UI スレッドに返さないとダメでした * if (displayTo.InvokeRequired) * displayTo.Invoke(new Action<IAsyncResult>(DisplayResult), ar); * */ }
みたいなかんじで、DisplayResult() がどこから呼び出されたかわかったものじゃないので、WinForms や WPF に代表される STA アプリではどのメソッドがどこで呼ばれるかわかりにくい状態でした。EAP では実行結果は非同期の呼び出し元(正確には所属コンテキスト)の元で実行されるため、
{ // Logic を呼び出し、結果が出たら TextBox へ表示する logic.Completed += DisplayResult; logic.DoAsync(); } private void DisplayResult(object sender, LogicCompletedEventArgs e) { // ここは呼び出し元コンテキストなので、直接処理しても大丈夫 this.TextBox.Text = e.Result; }
この仕組みの登場によって .NET 1.0/1.1 時代にあった Control.Invoke 病は完治するかに思えましたが、実質的なディスパッチャが WinForms の UI スレッドぐらいだったのが災いしたのか、既存ライブラリの書き換えが進まなかった等の原因で、あまり普及しなかったように思います。結果として、未だに UI スレッドの呼び出しにはまる人が後を絶えません。
一応、非同期可能オブジェクト側を簡単に紹介。
public class AsyncObject { // このオブジェクトが所属するコンテキスト private SynchronizationContext context; public AsyncObject() { // 一般的に、作成されたコンテキストに所属します。 this.context = SynchronizationContext.Current; } // サフィックス Async のメソッドと、サフィックス Completed のイベントが目印 public void LogicAsync(T1 param1, T2 param2) { ... } public event EventHandler<LogicCompletedEventArgs> LogicCompleted; protected virtual void OnLogicCompleted(TResult result) { var handler = this.LogicCompleted; if (handler != null) handler(this, new LogicCompletedEventArgs(result)); } private void BackgroundWorker(T1 param1, T2 param2) { /* ... 時間のかかる処理 ... */ // 処理が終わったら、シンコンさんイラッ…じゃなくてイッテラッシャイ this.context.Post(this.OnLogicCompleted, result); } // Logic の結果を保持するイベント引数 public class LogicCompletedEventArgs : EventArgs { internal LogicCompletedEventArgs(TResult result) { this.Result = result; } public TResult Result { get; private set; } } }
使う側はスレッド違いに悩まされないで済むのが便利ですが、ちょっと面倒ですね。
TPM (Task Asynchronous Pattern)
.NET 4 で追加された Task による非同期処理。Task そのものは非同期処理のための仕組みではなく、並列処理のための仕組みで、.NET 4 以前の ThreadPool に近い存在です。ThreadPool がシンプルなジョブのキューイングだけを機能として持っているのに対し、Task はスケジューリングやキャンセル、親子関係や長時間実行などなど、多くの機能を備えています。非同期呼び出しの実体が Task であることは、こういった Task の機能を活用できて便利になりました。
obj1.LogicAsync() // まず obj1 の Logic を実行し、 .ContinueWith(result => obj2.LogicAsync(result)) // その結果を使って obj2 の Logic 実行する .Start();
WPF ディスパッチャ
.NET 1.0 に APM があり .NET 2.0 で EAP が増え、.NET 4 には TPM がある。あれ? 何か忘れていませんか? そう、.NET 3.0 でも非同期の仕組みは追加されています。といっても、.NET 3.0/3.5 は CLR の更新を持たない拡張ライブラリとしての側面が強いのですが、非同期処理の仕組みもやはりそのようになっています。つまり、.NET 2.0 で導入された EAP の拡張 System.Windows.Threading 名前空間でした。.NET 3.0 で追加された中でもっとも基本となる System.Windows.Threading.DispatcherObject、さらにその派生クラスである System.Windows.Freezable が利用する側に見える中心的な役割をし、System.Windows.Threading.Dispatcher が SynchronizationContext に近い役割をします。これらは名前空間からも想像できるかもしれませんが、WPF における非同期処理のために追加されています。*1
WinForms と同様に、この拡張で導入された WPF のディスパッチャは、UI スレッドを EAP に対応させ EAP を利用した非同期メソッドの呼び出し結果を、適切な UI スレッドで受け取ることができるようになっています。WPF のディスパッチャに固有の機能として、コンテキストに送り込むメッセージに対して優先度*2を付与することができるようになっています。たとえば、非同期処理が比較的短時間で終わってしまった場合、その処理の完了を待つストーリーボードに配置されたプログレスバー アニメーションが終わるまで完了を通知しないが、エラーの通知は即座に行う…といった調整が可能になっているのです。他には、ユーザの入力を処理するよりも低い優先度を指定することで、分速800打でテキストボックスにタイピング中にエラーダイアログを表示してしまって、あっという間に閉じられたとかいうトラブルを防ぐことなんかもできます。
Freezable さんもすごいんですが、そろそろ眠たくなったのと、文章量的にもこれ以上書いても……なので、Freezable さんに興味がある方は後3回変身するまでお待ちください。