人間はもともと非同期な生き物です。このことは、私たちがアプリの反応に何を期待するかに直接影響を与えます。Windows ランタイム (WinRT) では、高速で滑らかな Metro スタイル アプリを構築するための一級市民としてこの非同期性が採用されています。Metro スタイル アプリを構築する場合、ある時点で非同期コードを記述する必要があります。この記事では、WinRT で非同期プログラミングがなぜ非常に一般的になっているかについてお話しし、アプリで非同期プログラミングを使用する基本的な方法やそのしくみの背景について説明します。

高速で滑らかなアプリは反応性が高くなければならない

Windows アプリを使っていて応答が止まったことや、アプリがグレー表示になってドーナツ型のカーソルが回転したことは何回くらいありますか。間違いなく、予想される最悪の時間に思えます。さらに悪いことに、このような状況に陥ると、努力してきた作業がたくさん失われてしまう可能性があります。

ユーザーは、アプリがすべての対話的操作に応答することを期待します。お気に入りのニュース閲覧アプリを使うとき、ニュース フィードを追加したり、新しい記事を読んだり、新しい記事を保存したりします。アプリがインターネットから最新の記事を取得しているときでも、ユーザーがこれらの操作をすべて行うことができる必要があります。

これは、ユーザーがタッチを使ってアプリを操作しているときは特に重要です。ユーザーは、アプリが "指に吸い付かない" ことに気付きます。パフォーマンス上の小さい問題でも、ユーザー エクスペリエンスが低下し、高速で滑らかな感覚が失われる可能性があります。

ユーザーの入力時にアプリの応答が停止する場合、アプリは高速で滑らかとは言えません。では、アプリが応答を停止するのはなぜでしょうか。主な理由は、アプリが同期的であるということです。ある処理が終わるのを待っているとき (インターネットからデータを取得するなど)、ユーザーの入力には応答できません。

最近のアプリの多くは、ソーシャル Web サイトに接続したり、クラウドにデータを保存したり、ハード ディスク上のファイルを操作したり、他のガジェットやデバイスと通信したりします。これらの要因によって予測できないレイテンシが生じると、高速で滑らかなアプリの作成が難しくなることがあります。正しく構築しないと、アプリが外部環境の待機に使う時間が長くなり、ユーザーのニーズへの応答に使う時間が短くなります。

私たちが Windows ランタイム (WinRT) の API を設計し始めた頃、いつでもつながるこの世界に対応することが中心的な原則でした。高速で滑らかなアプリにつながる強力な API サーフェスを既定で提供することが重要と感じていました。

これらの目標を達成するため、Windows ランタイムで I/O にバインドされる可能性がある多くの API を非同期にしました。同期的に記述した場合、視覚的なパフォーマンスが低下する可能性が高いものがあります (実行に 50 ミリ秒以上長く時間がかかる可能性があるなど)。API へのこのような非同期アプローチにより、高速で滑らかなコードを既定で記述できるようになり、Metro スタイル アプリ開発におけるアプリの応答性の重要性が高まります。

非同期パターンに慣れていない方は、WinRT の非同期性を、ある人に電話越しにコールバック番号を教えることに置き換えてみてください。その人にコールバック番号を教えて電話を切り、必要な他の仕事を続けます。その人があなたと話す準備ができたら、教えてもらった番号であなたにコールバックできます。これが WinRT における非同期性の本質的なしくみです。

WinRT の非同期的な性質についてよく理解するため、まずこれらの非同期 API を直接的な方法で使う方法を見てみます。次に、WinRT に導入された非同期プリミティブ (新しい言語機能はその上に構築されました) とそのしくみについて具体的に見てみましょう。

使用方法: 非同期性に関する開発者向けの新しい機能強化

以前は、非同期コードの記述は難しい作業でした。Windows 8 Metro スタイル アプリ用のツールを構築している私たちのチームは、最近のリリースにおいてこの問題を解決する点で大きな成果を挙げました。.NET Framework 4.0 では、タスク並列ライブラリを導入しました。次に、await キーワードを C# と Visual Basic に導入しました。C++ には、並列パターン ライブラリ (PPL) などのテクノロジが採用されました。また、JavaScript には Promises (英語) などのすばらしいツールがあります。

これらのテクノロジはそれぞれ、非同期コードを記述する直接的な方法を示しています。これらのテクノロジすべてが利用している標準的な非同期パターンを具体的に紹介すれば、使っているプログラミング言語に関係なく、皆さんが Metro スタイル アプリを構築するときにこれらのテクノロジを使うことができるようになります。

お気に入りのニュース アプリの場合、ニュース記事の取得中に応答性を維持するコードは、Windows.Web.Syndication.SyndicationClient API を使ったかなりシンプルなものです。RetrieveFeedAsync を呼び出すと、結果がすぐに返されます。インターネットから結果が返ってくるまでには長い時間がかかることがあるため、これはすばらしいことです。その間も、UI はユーザーの操作を受け付けます。

黄色でハイライトされたコードは、RetrieveFeedAsync の呼び出しが完了した後に実行されます。"やめたところから再開する" ことについてまったく考えなくてよいのです。また、コードは同期的であるかのように直接的な方法で記述されます。

RSS フィードを非同期的に取得する JavaScript コード:


var title;
var feedUri = new Windows.Foundation.Uri("http://www.devhawk.com/rss.xml");
var client = new Windows.Web.Syndication.SyndicationClient();

client.retrieveFeedAsync(feedUri).done(function(feed) {
title = feed.title.text;
});
 
RSS フィードを非同期的に取得する C# コード:
var feedUri = new Uri("http://www.devhawk.com/rss.xml");
var client = new Windows.Web.Syndication.SyndicationClient();
var feed = await client.RetrieveFeedAsync(feedUri);
var title = feed.Title.Text;

RSS フィードを非同期的に取得する VB コード:

Dim feedUri = New Uri("http://www.devhawk.com/rss.xml");
Dim client = New Windows.Web.Syndication.SyndicationClient();
Dim feed = Await client.RetrieveFeedAsync(feedUri);
Dim title = feed.Title.Text;
 

RSS フィードを非同期的に取得する C++ コード:

auto feedUri = ref new Windows::Foundation::Uri("http://www.devhawk.com/rss.xml");
auto client = ref new Windows::Web::Syndication::SyndicationClient();

task<SyndicationFeed^> retrieveTask(client->RetrieveFeedAsync(feedUri));
retrieveTask.then([this] (SyndicationFeed^ feed) {
this->title = feed->Title->Text;
});

 

スレッド、コンテキスト切り替え、ディスパッチャーなどについては何も記述されていません。非同期コードの記述に関する開発者向けの新しい機能強化により、自動的に処理されます。非同期 API 呼び出しの後のコードは、元の呼び出しが行われたのと同じコンテキストで自動的にコールバックされます。これは、UI スレッドに戻ることを気にしなくても、先に進んで UI を結果によって更新できることを意味します。

使用方法: エラー処理

もちろん、API 呼び出しが失敗することもあります (RSS フィードの取得中にネットワーク接続が失われるなど)。本当に安定させるには、エラーがあった場合の回復力を高める必要があります。言語の非同期機能を使うと、エラー処理がより直接的になります。これを行うメカニズムは言語により異なります。

JavaScript の場合、Promise チェーンを常に done で終わらせることをお勧めします。この結果、Promise チェーンで捕捉された例外がすべて開発者から見えるようになります (エラー ハンドラーで報告、つまりスローされます)。done は、then と同じシグネチャです。ですから、エラーを処理するには、エラー デリゲートを done() に渡すだけでかまいません。

 

var title;
var feedUri = new Windows.Foundation.Uri("http://www.devhawk.com/rss.xml");
var client = new Windows.Web.Syndication.SyndicationClient();

client.retrieveFeedAsync(feedUri).done(function(feed) {
title = feed.title.text;
}, function(error) {
console.log('an error occurred: ');
console.dir(error);
}
);


C# または Visual Basic で例外を処理するには、現在同期コードで行っているように try/catch ブロックを使います。

 

var title;
var feedUri = new Uri("http://www.devhawk.com/rss.xml");
var client = new Windows.Web.Syndication.SyndicationClient();

try
{

var feed = await client.RetrieveFeedAsync(feedUri);
title = feed.Title.Text;
}
catch (Exception ex)
{
// An exception occurred from the async operation
}


C++ で非同期エラーの処理に最もよく使われる方法は、例外をスローする別のタスク ベースの継続を使う方法です (C++ でエラーを処理する他の方法については、デベロッパー センターの「C++ での非同期プログラミング」(英語) で説明されています)。

auto feedUri = ref new Windows::Foundation::Uri("http://www.devhawk.com/rss.xml");
auto client = ref new Windows::Web::Syndication::SyndicationClient();

task<SyndicationFeed^> retrieveTask(client->RetrieveFeedAsync(feedUri));
retrieveTask.then([this] (SyndicationFeed^ feed) {
this->title = feed->Title->Text;
}).then([] (task<void> t) {
try
{
t.get();
// .get() didn't throw, so we succeeded
}
catch (Exception^ ex)
{
// An error occurred
}
})
;


非同期に関するこれらの改良点 (サポートの中止や向上のような他のシナリオを含みます) の使用方法の詳細については、以下を参照してください。

しくみ: WinRT 非同期プリミティブ

非同期はかなり強力ですが、皆さんのために不思議なことがたくさん起こっているように思えるかもしれません。何が起きているかを説明するため、WinRT における非同期性のしくみについてもう少し詳しく見てみましょう。最初のステップとして、モデルが構築されているプリミティブについて詳しく調べます。皆さんのほとんどはこのプリミティブをまったく使う必要がないでしょう (使う必要があるシナリオを見つけた場合、ぜひフィードバックをお寄せください)。

まずは C# コードについて、RetrieveFeedAsync の実際の戻り値の型を見てみましょう (ここでは C# Intellisense に表示されています)。

 RetrieveFeedAsync メソッドが表示された Intellisense
 図 1. RetrieveFeedAsync メソッドが表示された Intellisense

RetrieveFeedAsync は、IAsyncOperationWithProgress インターフェイスを返します。IAsyncOperationWithProgress は、WinRT の主要な非同期プログラミング サーフェスを定義する 5 つのインターフェイスである IAsyncInfo (英語)、IAsyncAction (英語)、IAsyncActionWithProgress (英語)、IAsyncOperation (英語)、および IAsyncOperationWithProgress (英語) の 1 つです。

WinRT の非同期モデルが構築されている主要なインターフェイスは IAsyncInfo です。この主要なインターフェイスは、現在のステータスのような非同期操作、操作をキャンセルする機能、失敗した操作のエラーなどのプロパティを定義します。

既に述べたとおり、非同期操作により結果が返されることがあります。WinRT の非同期操作では、実行中に進捗状況が報告されることもあります。前の他の 4 つの非同期インターフェイス (IAsyncActionIAsyncActionWithProgressIAsyncOperation、および IAsyncOperationWithProgress) は、さまざまな結果と進捗状況の組み合わせを定義します。

 

さまざまな結果と進捗状況の組み合わせを示す図   
図 2. さまざまな結果と進捗状況の組み合わせ

Windows ランタイムで主要な非同期プリミティブを提供することにより、C#、Visual Basic、C++、JavaScript で WinRT の非同期操作をわかりやすい方法で計画できるようになりました。

注: コールド スタートからホット スタートへの移行

//build でリリースされた Windows 8 Developer Preview では、WinRT におけるすべての非同期操作がコールド スタートであったため、すぐには実行を開始しませんでした。開発者が明示的に開始するか、C#、Visual Basic、JavaScript、または C++ の非同期機能を使って暗黙的に開始する必要がありました。これは直感的でないので苦痛と混乱が生じることがある、というフィードバックを複数の方からいただきました。

Windows 8 Consumer Preview リリースでは、WinRT の非同期プリミティブから Start() メソッドが削除されたことに気付かれると思います。今回、非同期操作がすべてホット スタートになりました。したがって、非同期メソッドは呼び出し元に返される前に操作を開始します。

これは、Consumer Preview に加えられた多くの変更の 1 つにすぎません。皆さんからいただいたさまざまな貴重なフィードバックに基づいて、開発者プラットフォームを微調整しました。

しくみ: 非同期プリミティブによる結果、キャンセル、エラー

非同期操作は Started 状態から始まり、他の 3 つの状態 (Canceled、Completed、Error) のいずれかに進みます。非同期操作の現在の状態は、非同期操作の Status プロパティに反映されます (AsyncStatus 列挙型により表されます)。

非同期操作で最初に行うことは、completed ハンドラーの接続です。completed ハンドラー内から、結果を取得して使うことができます。

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> op;
op = client.RetrieveFeedAsync(feedUri);

op.Completed = (info, status) =>
{
SyndicationFeed feed = info.GetResults();
UpdateAppWithFeed(feed);
};


場合によっては、操作をキャンセルしたいことがあります。これは、非同期操作で Cancel メソッドを呼び出して行うことができます。

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> op;
op = client.RetrieveFeedAsync(feedUri);
op.Cancel();


Completed ハンドラーは、完了、キャンセル、エラーのどの状態かに関係なく、非同期操作に対して常に呼び出されます。GetResults は、非同期操作が完了した場合のみ呼び出します (操作がキャンセルしたり、エラーが発生したりしない場合)。これは、ステータス パラメーターを調べることで判断できます。

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> op;
op = client.RetrieveFeedAsync(feedUri);
op.Cancel();

op.Completed = (info, status) =>
{
if (status == AsyncStatus.Completed)
{
SyndicationFeed feed = info.GetResults();
UpdateAppWithFeed(feed);
}
else if (status == AsyncStatus.Canceled)
{
// Operation canceled
}
};


操作が失敗した場合、このコードはまだ安定していません。キャンセルと同じように、同じ非同期操作によってエラー報告がサポートされます。AsyncStatus を使って Completed と Canceled を区別するだけでなく、非同期操作の失敗も判断します (つまり AsyncStatus.Error)。具体的な失敗コードは、非同期操作の ErrorCode プロパティで見つけることができます。

op.Completed = (info, status) =>
{
if (status == AsyncStatus.Completed)
{
SyndicationFeed feed = info.GetResults();
UpdateAppWithFeed(feed);
}
else if (status == AsyncStatus.Canceled)
{
// Operation canceled
}
else if (status == AsyncStatus.Error)
{
// Error occurred, Report error
}
};


しくみ: 非同期プリミティブによる進捗状況の報告

WinRT の非同期操作によっては、実行中に進捗状況の通知が生成されます。これらの通知を使って、非同期操作の現在の進捗状況をユーザーに報告できます。

WinRT では、進捗状況の報告は、IAsyncActionWithProgress<TProgress> インターフェイスと IAsyncOperationWithProgress<TResult, TProgress> インターフェイスによって処理されます。各インターフェイスには Progress イベントが含まれており、これを使って非同期操作の進捗状況の報告を取得することができます。

これを利用するプログラミング モデルは、Completed イベントの利用とほぼ同じです。シンジケーション フィードを取得する例では、合計何バイト取得されたかを報告できます。

 

op = client.RetrieveFeedAsync(feedUri);

float percentComplete = 0;
op.Progress = (info, progress) =>
{
percentComplete = progress.BytesRetrieved /
(float)progress.TotalBytesToRetrieve;
};


この場合、2 つ目のパラメーター (progress) の型は RetrievalProgress です。この型には、進行中に進捗状況の報告に使われる 2 つのパラメーター (bytesRetrievedtotalBytesToRetrieve) が含まれます。

より詳細な資料 (WinRT での非同期プログラミング、または一般的な非同期性) については、以下を確認してください。

終わりに

WinRT で非同期性がかなり一般的になっているということで興味深いのは、コードを構築してアプリを設計する方法に与える影響です。もっと関連性の緩い方法でアプリについて考え始めることができます。この分野に関してフィードバックをお持ちの場合はぜひお聞かせください。ここかデベロッパー センターのフォーラムにコメントすることができます。

皆さんの顧客に Windows 8 アプリを気に入っていただけると幸いです。アクティビティによってアプリに新鮮な情報が表示され続けるようにしたいと思います。また、アプリをもっと簡単に作れるようにしたいと思います。さらに、すばらしいユーザー エクスペリエンスを実現しながら、ソーシャル Web サイトに接続したり、クラウドにデータを保存したり、ハード ディスク上のファイルを操作したり、他のガジェットやデバイスと通信したりするなどの操作を、簡単に行うことができるようにしたいと思います。

 

--Jason Olson

Windows プログラム マネージャー