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 風に書けるようにしてみました。

awaitreturn の変わりに 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 の戻り値 3init2 で取得できています。doXhrTwice が終わったあと、`doXhr end` が出力されています。

もちろん、処理は同期的に書いていますが、この間、init2 関数は yield で処理をシステムに返しているので、UI はブロックされません。async の実装の中で、doXhrTwice 完了時に yield された処理を再開しています。

asyncawaitable の実装も含めたソースは https://gist.github.com/1232433 に置いています。

なんか楽しい

JavaScript ではクロスブラウザ的な意味で実用的ではないけれど、何か楽しくコードが書けそうです。

Python には yield が実装されているので、同じ仕組みをそのまま流用できそうです。