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

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

非同期メソッド入門 (10) - WinRTとの相互運用

今回はWindows 8に新しく搭載されたAPI、WinRT (=Windows Runtime) との相互運用性について見ていきます。WinRTはMetroスタイルアプリケーションを作るためのAPIで、これまでのWin32 APIとはかなり毛色が違い、オブジェクト指向なAPIに生まれ変わっています。また、WinRTはファイルのIOやネットワーク通信を始めとする多くの機能 (=より具体的には、50ミリ秒以上かかる可能性のある処理) を非同期APIとして提供しています。これは、Metroスタイルアプリケーションがタブレットなどのタッチデバイスをメインターゲットにしていることが理由です。

Windows PhoneiPhoneAndroidなどのスマートフォンをはじめ、iPadやAndroidタブレットなどのタッチデバイスを触ったことがある方は経験があるかと思いますが、指での操作にUIが付いてこないときの不快感はかなりのものです。マウス/キーボードで操作しているときなどとは比べ物にならないくらい我慢がききません。操作にUIが付いてこないのはUIスレッドで長い処理が行われているからです。より良いユーザーエクスペリエンスを提供するためにはUIを軽快にする必要があります。Metroスタイルアプリケーションでは、このような一貫したユーザーエクスペリエンスを提供するため、APIレベルで非同期処理を強いていると言えます。

WinRTの非同期処理の概要

WinRTが提供する非同期操作はすべて、以下の4つのうちのいずれかのインターフェースを実装しています。4つもあって身構えそうなものですが、戻り値の有無と進捗通知の有無のマトリックスに分かれているだけで、特に難しいことはありません。

AsyncInterface

これらのインターフェースはすべて、完了時に呼び出されるデリゲートを1度だけ設定可能なCompletedプロパティを持っています。素敵なことに、このCompletedプロパティに設定したデリゲートは非同期処理が完了した後に設定してもCallbackしてもらえます。また、これらのインターフェースは共通してIAsyncInfoインターフェースを実装しており、エラーコードや非同期処理の状態の取得、キャンセル機能などがサポートされています。

以下に、WinRTの非同期インターフェースを使ってTwitterのタイムラインを表示する簡単な実装例を示します。Callbackを設定してどうこうするというのは、非同期メソッド入門 (1) - 非同期処理の歴史で紹介したEAP (=Event-based Asynchronous Pattern) と似ています。

public void UpdateFeed()
{
    var uri             = new Uri("http://search.twitter.com/search.atom?q=xin9le");
    var client          = new SyndicationClient();
    var operation       = client.RetrieveFeedAsync(uri);
    var context         = SynchronizationContext.Current;
    operation.Completed = delegate  //--- コールバックを設定
    {
        context.Post(_ =>  //--- コールバックはUIスレッド上で動かす
        {
            switch (operation.Status)  //--- 状態を見て書き分ける
            {
                case AsyncStatus.Completed:
                    {
                        var feed = operation.GetResults();
                        this.DataContext = feed.Items.Select(item => new
                        {
                            Image       = item.Links[1].Uri.OriginalString,
                            Author      = "@" + item.Authors[0].Name,
                            Content     = item.Title.Text,
                            PublishDate = item.PublishedDate.LocalDateTime.ToString(),
                        });
                    }
                    break;

                case AsyncStatus.Error:
                    {
                        Debug.WriteLine(operation.ErrorCode.Message);
                    }
                    break;

                case AsyncStatus.Canceled:
                    {
                        //--- do nothing
                    }
                    break;
            }
        }, null);
    };
}

Taskへの変換

上の実装例はもちろん間違いではないのですが、せっかく.NET Framework 4.5でasync/awaitという強力な非同期メソッドの構文が搭載されるなら、WinRTの非同期処理もこれを使って実装したいと思うことでしょう。どうしたら非同期インターフェースをawaitすることができるか。これを実現する最も簡単な方法は、非同期インターフェースをTask型に変換してしまうことです。

以下、一例として戻り値を持つ非同期インターフェースであるIAsyncOperation<T>Task<T>に変換する拡張メソッドを示します。TaskCompletionSource<T>を利用し、非同期処理の完了状態に合わせて遷移を変更しています。

public static Task<T> AsTask<T>(this IAsyncOperation<T> operation)
{
    var tcs             = new TaskCompletionSource<T>();
    operation.Completed = delegate  //--- コールバックを設定
    {
        switch (operation.Status)   //--- 状態に合わせて完了通知
        {
            case AsyncStatus.Completed:  tcs.SetResult(operation.GetResults());  break;
            case AsyncStatus.Error:      tcs.SetException(operation.ErrorCode);  break;
            case AsyncStatus.Canceled:   tcs.SetCanceled();                      break;
        }
    };
    return tcs.Task;  //--- 完了が通知されるTaskを返す
}

このAsTaskメソッドのようなコンバーターを利用することで、先ほどの例は以下のように書き換えることができるようになります。非常にコンパクトです。

public async void UpdateFeed()
{
    try
    {
        var uri    = new Uri("http://search.twitter.com/search.atom?q=xin9le");
        var client = new SyndicationClient();
        var feed   = await client.RetrieveFeedAsync(uri).AsTask();
        this.DataContext = feed.Items.Select(item => new
        {
            Image       = item.Links[1].Uri.OriginalString,
            Author      = "@" + item.Authors[0].Name,
            Content     = item.Title.Text,
            PublishDate = item.PublishedDate.LocalDateTime.ToString(),
        });
    }
    catch (Exception e)
    {
        Debug.WriteLine(e.Message);
    }
}

直接await

ここまで簡潔になったとはいえ、.AsTask();などと毎回×2書きたくはありません。どうにかして.AsTask();と書かずにawaitする方法はないものか、と考えます。ここで、非同期メソッド入門 (8) - コンパイラ要件で見てきたawaitするためのコンパイラ要件を思い出してみましょう。そうです、Awaiterを返すGetAwaiterメソッドを持てば良いのです。そしてそれは拡張メソッドでもOKでした。ということで、以下のようなGetAwaiter拡張メソッドを実装します。

public static TaskAwaiter<T> GetAwaiter<T>(this IAsyncOperation<T> operation)
{
    return operation.AsTask().GetAwaiter();
}

これにより、以下のふたつは同等ということになります。つまりWinRTの非同期インターフェースは、上記のような拡張メソッドがあればすべてそのままawait可能ということです。

var hoge = await operation.AsTask();
var fuga = await operation;

.NET Framework標準での提供

しかしながら、「WinRTの非同期インターフェースをawaitするために毎度AsTaskやGetAwaiterなどという機能を実装しなきゃいけないのか?」というともちろんそのようなことはなく、.NET Framework標準でこれらの変換メソッドが提供されています。

#region アセンブリ System.Runtime.WindowsRuntime.dll, v4.0.0.0
// C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETCore\\v4.5\\System.Runtime.WindowsRuntime.dll
#endregion

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;

namespace System
{
    public static class WindowsRuntimeSystemExtensions
    {
        public static Task AsTask(this IAsyncAction source);
        public static Task AsTask<TProgress>(this IAsyncActionWithProgress<TProgress> source);
        public static Task<TResult> AsTask<TResult>(this IAsyncOperation<TResult> source);
        public static Task<TResult> AsTask<TResult, TProgress>(this IAsyncOperationWithProgress<TResult, TProgress> source);
        public static Task AsTask(this IAsyncAction source, CancellationToken cancellationToken);
        public static Task AsTask<TProgress>(this IAsyncActionWithProgress<TProgress> source, CancellationToken cancellationToken);
        public static Task AsTask<TProgress>(this IAsyncActionWithProgress<TProgress> source, IProgress<TProgress> progress);
        public static Task<TResult> AsTask<TResult>(this IAsyncOperation<TResult> source, CancellationToken cancellationToken);
        public static Task<TResult> AsTask<TResult, TProgress>(this IAsyncOperationWithProgress<TResult, TProgress> source, CancellationToken cancellationToken);
        public static Task<TResult> AsTask<TResult, TProgress>(this IAsyncOperationWithProgress<TResult, TProgress> source, IProgress<TProgress> progress);
        public static Task AsTask<TProgress>(this IAsyncActionWithProgress<TProgress> source, CancellationToken cancellationToken, IProgress<TProgress> progress);
        public static Task<TResult> AsTask<TResult, TProgress>(this IAsyncOperationWithProgress<TResult, TProgress> source, CancellationToken cancellationToken, IProgress<TProgress> progress);
        public static TaskAwaiter GetAwaiter(this IAsyncAction source);
        public static TaskAwaiter GetAwaiter<TProgress>(this IAsyncActionWithProgress<TProgress> source);
        public static TaskAwaiter<TResult> GetAwaiter<TResult>(this IAsyncOperation<TResult> source);
        public static TaskAwaiter<TResult> GetAwaiter<TResult, TProgress>(this IAsyncOperationWithProgress<TResult, TProgress> source);
    }
}

Metroスタイルアプリケーションを作る際は、これらを率先して利用しましょう!

awaitをより高度に扱う

GetAwaiterメソッドの公開により非同期インターフェースを直接awaitすることができるのであればAsTaskメソッドは特に必要ないのでないか、というとそのようなことはありません。以下のケースのように、よりawaitを高度に利用する場合は明示的にAsTaskを利用する必要があります。

  • 非同期処理をキャンセルする
  • 進捗通知を行う
  • 複数の非同期処理がすべて完了するまで待機する
  • 継続処理をUIスレッドに戻さない
  • etc...

Task型としての機能をフル活用したい場合はAsTask、と覚えておけばよいかと思います。

WinRTの非同期インターフェースへの変換

MetroスタイルアプリケーションはC#/VB + XAML、C++/CX + XAML、JavaScript + HTML5と、多く言語で実装することができます。そしてその核となるWinRTは、言語プロジェクションを介したWinMD (=Windows Metadata) という仕組みのおかげで、すべての言語で同じように利用することができます。一定の規約に従えば (=言語プロジェクションの適用範囲内であれば) 言語を超えた共通ライブラリを自作することもできます。このとき共通ライブラリの作成にあたって、以下のような要件が出るかもしれません。

  • 非同期処理を入れたい
  • C#で実装したい

C#で作るならasync/awaitを使って非同期処理を実装し、それを公開したいと思うことでしょう。しかし、共通ライブラリが公開すべき非同期処理のインターフェースは、冒頭の4つである必要があります。つまり、先程WinRTの非同期インターフェースからTask型へ変換したのとは逆方向の変換のニーズがあることになります。そして、これらもまた.NET Framework標準で変換メソッドが提供されています。

まず、単純なIAsyncActionIAsyncOperation<T>への変換は、それぞれTaskとTask<T>からの変換となります。

#region アセンブリ System.Runtime.WindowsRuntime.dll, v4.0.0.0
// C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETCore\\v4.5\\System.Runtime.WindowsRuntime.dll
#endregion

using Windows.Foundation;
using System.Threading.Tasks;

namespace System
{
    public static class WindowsRuntimeSystemExtensions
    {
        public static IAsyncAction AsAsyncAction(this Task source);
        public static IAsyncOperation<TResult> AsAsyncOperation<TResult>(this Task<TResult> source);
    }
}

また、キャンセル機能や進捗通知など、より高度な機能をサポートする変換にはAsyncInfo.Runメソッドを利用します。

#region アセンブリ System.Runtime.WindowsRuntime.dll, v4.0.0.0
// C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETCore\\v4.5\\System.Runtime.WindowsRuntime.dll
#endregion

using System;
using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;

namespace System.Runtime.InteropServices.WindowsRuntime
{
    public static class AsyncInfo
    {
        public static IAsyncOperationWithProgress<TResult, TProgress> Run<TResult, TProgress>(Func<CancellationToken, IProgress<TProgress>, Task<TResult>> taskProvider);
        public static IAsyncActionWithProgress<TProgress> Run<TProgress>(Func<CancellationToken, IProgress<TProgress>, Task> taskProvider);
        public static IAsyncOperation<TResult> Run<TResult>(Func<CancellationToken, Task<TResult>> taskProvider);
        public static IAsyncAction Run(Func<CancellationToken, Task> taskProvider);
    }
}

参考資料

今回の内容は以下の記事を参考にしています。内部実装やしくみなどについてより詳しく知りたい場合は、是非ご一読ください。

WinRTとawaitを掘り下げる
.NETタスクをWinRT非同期処理として公開する