WinRT の async/await コーディングがおもしろい(のでJavaScriptで真似してみた)
Windows 8 開発者プレビューでの開発を試していたところ、非同期プログラミングを簡単に書ける await
キーワードが面白かったので紹介します。
Windows 8 用に Metro アプリを開発する場合、WinRT(Windows Runtime)というフレームワークを使ってプログラミングします。
WinRT では、UI 応答速度を上げるための工夫として、少しでも時間がかかる可能性のある処理は、非同期の API のみが提供されています。
なんかめんどくさそうですよね。
いえいえ、そんなことはありません。.NET 5 から導入される async
/await
キーワードでとても簡単に書けてしまいます。
HelloWorld アプリケーションのサンプル
たとえば、HelloWorld アプリケーションでは次のようなサンプルが紹介されています。
// UI を表示するページのコンストラクタ // UI スレッドで実行される public MainPage() { InitializeComponent(); // RSS/Atom を読み取る GetFeedAsync("http://windowsteamblog.com/windows/b/developers/atom.aspx"); } // 非同期に RSS/Atom を読み取るメソッド // async キーワードがついているので、非同期で // 実行される可能性がある // (メソッドの完了を待たずに、呼び出し元に // 処理を返すことがある) private async Task GetFeedAsync(string feedUriString) { SyndicationClient client = new SyndicationClient(); Uri feedUri = new Uri(feedUriString); try { // 非同期で RSS/Atom を読み取る // await キーワードがあるので、GetFeedAsync() // メソッドは処理の完了を待たずに、いったん // 呼び出し元に return する SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri); // ここから先は、非同期で RSS/Atom を // 読み取り完了時の処理 FeedData feedData = new FeedData(); feedData.Title = feed.Title.Text; foreach (SyndicationItem item in feed.Items) { // RSS の各要素の処理を行う } } catch (Exception ex) { // 非同期での RSS/Atom の読み取りに // 失敗したときの処理 TitleText.Text = ex.Message; } }
コメントで説明してしまいましたが、詳しく見ていきましょう。
await
でいったん呼び出し元に戻る
await
の部分の処理の構造は次のようになっています。
try { // 非同期で RSS/Atom を読み取る SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri); // 成功時の処理 } catch (Exception ex) { // 失敗時の処理 }
同期処理のようなコードになっていますね。
しかし、実際には await
キーワードの場所で、メソッドは呼び出し元に処理を返します。その結果、非同期処理の実行中に UI をブロックすることはありません。
その後、RSS/Atom の読み取りが成功または失敗した段階で、メソッドの続きが実行されます。
成功時には、読み取り結果があたかも同期で実行したかのように戻り値として返され、成功時の処理を実行します。失敗時には例外が投げられるので、異常系の処理も簡単に書けます。
イメージ的には次のようなコードを書いているのと同じになります。
/* !!!! 実際には動かない擬似的なコード !!!! */ client.RetrieveFeedSuccess += (sender, e) => { // 成功時の処理 } client.RetrieveFeedError += (sender, e) => { // 失敗時の処理 } // 非同期で RSS/Atom を読み取る client.RetrieveFeedAsync(feedUri);
すばらしい!
非同期を同期的に書けるなんてすばらしい。
コールバックやイベントハンドラ地獄から抜け出せそうな予感!
ところで、どういう仕組みなのか?
ところで、await
キーワードはどのような仕組みで動いているのでしょうか。
WinRT の非同期メソッドは全て、Task
オブジェクト、Task<TResult>
オブジェクトを返すように実装されています。Task
クラス、Task<TResult>
クラスは、非同期処理を表現するクラスです。
自前のメソッドでも、async
キーワードをつけることで、void
を返すメソッドは戻り値 Task
として、型 T
を返すメソッドは戻り値 Task<T>
として扱われます。
.NET 5 では継続の仕組みが導入されたので、メソッドの処理の途中で、処理をぶった切ったり、再開したりすることができるようになっています。
async
/await
キーワードは、Task
クラス、Task<TResult>
クラスを利用しつつ、継続を利用してメソッドの処理を途中で中断したり、中断したところから続きをやり直したりするためのシンタックス シュガーになっているそうです。
JavaScript でも実現できるんじゃね?
ところで、JavaScript 1.7 からは yield
が使えるようになっています(ただし、現時点で動くのは Firefox のみ)。
そこで、yield
を使っても同じような仕組みを実現できるんじゃないかと思って作ってみました。
ほぼ形は似ていますが、XHR を使った Ajax 処理を await
風に書けるようにしてみました。
await
や return
の変わりに yield
を使っています。また、XHR を await
できるように変換する awaitable
関数を作っています。
var doXhrTwice = async(function(){ var xhr = awaitable(new XMLHttpRequest()); xhr.open('GET', location.href, true); var result = yield xhr.send(null); log("finished: " + result + ", status: " + xhr.status); xhr = awaitable(new XMLHttpRequest()); xhr.open('GET', "not_found.png", true); result = yield xhr.send(null); log("finished: " + result + ", status: " + xhr.status); yield 3; });
これの関数を普通に呼び出すと、ログ出力は次のようになります。
/* doXhr start * doXhr end * finished: true, status: 200 * finished: false, status: 404 */ function init1(){ log("doXhr start"); doXhrTwice(); log("doXhr end"); }
init1
から doXhrTwice
を呼んでもすぐに処理が返ってくるので、関数呼び出し直後に `doXhr end` が出力されます。ただ、doXhrTwice
の中では同期的に処理を書けています。
次に、doXhrTwice
を同期的に呼んでみます。
/* doXhr start * finished: true, status: 200 * finished: false, status: 404 * doXhr end: 3 var init2 = async(function(){ log("doXhr start"); var ret = yield doXhrTwice(); log("doXhr end: " + ret); });
doXhrTwice
の戻り値 3
を init2
で取得できています。doXhrTwice
が終わったあと、`doXhr end` が出力されています。
もちろん、処理は同期的に書いていますが、この間、init2
関数は yield
で処理をシステムに返しているので、UI はブロックされません。async
の実装の中で、doXhrTwice
完了時に yield
された処理を再開しています。
async
や awaitable
の実装も含めたソースは https://gist.github.com/1232433 に置いています。
なんか楽しい
JavaScript ではクロスブラウザ的な意味で実用的ではないけれど、何か楽しくコードが書けそうです。
Python には yield が実装されているので、同じ仕組みをそのまま流用できそうです。