« [本] アジャイルサムライ ~ アジャイルマインドの伝道書 | トップページ | [.NET Framework 4/4.5][C# 5] 非同期で動くビジネスロジックを作る »

2011年12月 6日 (火)

[C# Advent Calendar 2011] Win8 に備えて async / await を勉強してみよう

これは、 C# Advent Calendar 2011 の 12月 6日の記事です。
この記事では、 C# の新機能である async / await を解説します。


■ Windows 8 の Metro スタイル

20111205_win8

この画像は、 Windows 8 Developer Preview のスタート画面。
Windows Phone 7 から導入されたこの Metro スタイルが、 Windows 8 の標準になります。 おそらく多くの一般ユーザーは、 このスタート画面からアプリを使うようになり、 デスクトップを見ることは無くなることでしょう。

スタート画面のアイコンをクリックすると、 そのアプリが全画面で起動します。 Windows キー [ミ田] (Windows Phone では同じ記号のボタン) を押すと、 再びスタート画面に戻ります。

Metro スタイルのアプリケーションの UI は、 Silverlight に似た XAML で定義します。 .NET Framework のクラスライブラリは、 主に Windows Runtime (WinRT) と呼ばれる新しいものを利用します。
WinRT では、 ユーザーリソースに対するアクセスは細かく制限されていて、 ユーザーの許可が無いとアクセスできません。 アプリがアクセスしたいユーザーリソースの情報は、 ソースコードレベルでは Package.appxmanifesto に記述し、 バイナリーに組み込まれます。

20111205_vs11dp04
※ Package.appxmanifesto エディター画面の例。 かなり細かくアクセス対象が分類されているのが分かる。
※ ユニットテストのプロジェクトといえども、 この Package.appxmanifesto で許可を与えないとアクセスできないので注意。

■ Metro スタイルのアプリケーションは、 どうやって終わるのか?

Metro スタイルのアプリケーションには、 見慣れた右肩の [X] ボタンがありません。 使い終わったら、 ただ Windows キーを押してスタート画面に戻るだけです。 そのときにアプリケーションは Suspended (一時停止中) 状態にされ、 一定時間が経つと終了させられます。
※ 詳しくは、 このあたりをどうぞ。 ⇒ @IT: 開発者のためのBUILDレポート(前編)「次期Windows 8向け「Metroスタイル・アプリ」とは?」

一時停止中に終了させられてしまうのですから、 今までのように 「[X] ボタンのクリックを拾ってごにょごにょ…」 というのは出来ません。 Metro スタイルでは、 一時停止中に入るときに Suspending イベントが発生するので、 一時停止/終了時の処理はそこで行うことになります。

次は、 BasicToastsSample というサンプルの、 メインとなる App クラスのコードです。

コンストラクト時に SuspendingEventHandler を登録しています。 一時停止されようとするときに、 SuspendingEventHandler である OnSuspending() メソッドが呼び出されます。 また、 起動時/再開時に OnLaunched() が呼び出されます。

OnSuspending() メソッドでは、 プロジェクト内の SuspensionManager クラスの  SaveAsync() メソッドを呼び出して、 再開するときに必要になるデータの保存をし、 それが終わったら、 deferral.Complete() で 「もう停めて OK だよ!」 と通知を出しています。

■ C# 5 で追加される async / await キーワード

さて、 この OnSuspending() メソッドの頭に付いている async と、 SaveAsync() メソッドを呼び出しているところの await というのは、 何でしょう? これが、 C# 5 (Visual Studio 11) で導入される非同期プログラミングサポートのキーワードです。

Metro スタイル (というより、 WinRT ) では、 async / await が使われまくっています。 ので、 WinRT を使うには当然、 async / await を理解しておかねばなりません。

なんとか短く説明してみると…
await は、 yield return のように、 そこで処理を一旦中断して制御を返すことによって、 非同期実行を実現する。
async は、 そのメソッドに await が登場することを宣言する。 また、 返値を Task<T> 型にラップしてくれる。

■ async / await を試す環境

この記事を書いている時点では、 まだちょっとハードルが高いです。 以下のどれかを使います。
Visual Studio 11 Developer Preview
・ Visual Studio 2010 SP1 + Visual Studio Async CTP (Ver.3)
Windows 8 Developer Preview with developer tools English, 64-bit (x64)

※ 最後の Win8DP with Dev.Tools x64 は、 Oracle の VirtualBox で動作します。 この記事のキャプチャも、 64bit 版 Windows 7 上の VirtualBox です。 Win8DP with Dev.Tools x64 付属の VS11DP Express は、 Metro スタイルのアプリが作れるだけでなく、 ユニットテスト機能も付いています。
※※ 参考画像: http://twitpic.com/750yw8 http://twitpic.com/751srq http://twitpic.com/753g0r http://twitpic.com/753k30 http://twitpic.com/754e7l http://twitpic.com/75g6oxhttp://twitpic.com/75gpik http://twitpic.com/75i2hd
※ Async CTP Ver.3 はダウンロードページの説明によると、 Silverlight 5 (RC) や Windows Phone SDK 7.1 (Mango) でも利用可能です。

20111205_vs11dp01

■ WinRT の SyndicationClient を使ってみよう

新しく増えた WinRT の SyndicationClient クラス は、 RSS フィードを非同期で取得してくれます。 これを使って Amazon.co.jp のベストセラーなどのフィードを取ってくるコードを書いてみます。

【作りたいモノ】
クラス: FeedReader
メソッド: async Task<FeedData> ReadAsync(string rssUrl)
刺激と応答:
引数 rssUrl を刺激とし、そのサイトから取得してきた RSS フィードの内容を FeedData オブジェクト (プロジェクト内のクラス) に詰め替えて、 それを応答として返す。 ただし、 非同期実行。

そうすると、 こんなテストケースが書けます。

■ はじめての async / await

【FeedReader: ひとつめのテスト】

このテストコードには async / await を使っていません。

async 修飾子の付いたメソッドは、 Task<T> 型 (または、 void 代わりの Task 型) を返すので、 Task<T> で受け取ります。
・Task<T>.IsCompleted: 非同期実行が完了したか調べる
・Task<T>.Wait(): 非同期実行の終了を待つ
・Task<T>.Result: 非同期実行の結果を取得する

Task<T>.Result で <T> 型 (ここでは FeedData 型) が返ってくることに、 注目。 キャスト不要で、 ラクチンです。

ReadAsync(RssUrl) の呼び出しは、 すぐ (実際には 50mSec 程度) に制御が返ってきて、 その後の処理が実行されています。 task.Wait() を発行すると非同期実行の完了まで待たされます (実際には 500~1000mSec 程度)。

上のテストケースに通るように作った  ReadAsync() メソッドは、 次のようになります。

【FeedReader: 最初の実装】

コメントで※印を付けた行に await があります。 この行で、 非同期実行してくれる SyndicationClient の RetrieveFeedAsync() メソッドを呼び出しています。

単に (await 無しで) 呼び出しただけでは、 RetrieveFeedAsync() メソッドは直ちに Task<SyndicationFeed > を返してきて、 後続の処理が実行されてしまいます。 しかし、 非同期でのフィード取得が終わる前に feed.ToFeedData(); を実行してしまっては、 マズいのです。

そこで await キーワードの出番です。
後続の処理を実行せず、 RetrieveFeedAsync() メソッドを呼び出した直後に (yield return のように) 制御を呼び出し元に返します。 そして、 RetrieveFeedAsync() メソッドの非同期処理が終わってから、 後続の feed.ToFeedData(); を実行してくれるのです。

大事な事なので、 もういっぺん書きます。
await は、ウェイトしません。待たずにリターンします。
asynchronous が synchronous (「同期の」) の反対の意味 (同期しない) であるのと同様、 await も wait の反対の意味です。

ちなみに、 SyndicationClient クラスが同期バージョンの RetrieveFeed() というメソッドを持っていたら (実際にはありません)、 同期実行バージョンの ReadAsync() メソッドは次のように書けるはずです。

【FeedReader: 最初の実装 ~ 同期処理だったらこんな感じ】

async と await を消しただけですね!

なお、 ToFeedData() メソッドは SyndicationClient クラスにはありません。 次のように、 拡張メソッドとして書きました。

【FeedReader: ToFeedData() 拡張メソッド】

■ Task は Wait() しないとダメ?

async / await からちょっと離れますが、 非同期なメソッドから返された Task<T> は、 Wait() せずに使ったらどうなるのでしょうか? や、 Wait() の一行くらいちゃんと書けよ、って話ではありますが。 確かめておきましょう。

【FeedReader: ふたつめのテスト ~ Wait() を書かないとどうなる?】

なんと、 Wait() を書かずに Result プロパティを参照すると、 内部で完了まで待ってから値を返してくれます。

つまり、 次のように書けば、 あたかも同期メソッドのように扱えるわけです。
※ 製品コードで、 こんな台無しのコードは書かないように!!

【FeedReader: みっつめのテスト ~ 同期メソッドみたいに扱える】

■ async / await はテストコードでは使いにくい (今のところ!)

ところで、 上記のテストケース側で async / await を使っていないのは何故かと言うと。
現状では Visual Studio のテストランナーが async / await に対応しておらず、 await されたところでテストが終わってしまうんですね。 つまり、 await 後の Assert で失敗しても、 テストランナーはそれに気付かず、 全部 GREEN にしてくれちゃいます。

【FeedReader: 4つめのテスト ~ テストケースでは async / await は使えない】

20111205_vs11dp02
※ Test4 が 1ミリ秒で終わっている。 非同期実行が終わる前 (つまり、Assert する前) に、 テストが完了してしまっていることになる。
※ この挙動は、 製品版までに修正されることが望まれます。 製品コードは async / await で書いているのだから、 テストコードも同じように書きたいものです。

■ 非同期実行を複数併走させる

さて、 実際に欲しかったのは、 URL 文字列の配列を渡して、 FeedData のリストを返してくれるメソッドです。

【作りたいモノ】
クラス: FeedReader
メソッド: async Task<IList<FeedData>> ReadAsync(string[] rssUrls)
刺激と応答:
引数 rssUrls を刺激とし、それらのサイトから取得してきた RSS フィードの内容を FeedData オブジェクト (プロジェクト内のクラス) のリストに詰め替えて、 それを応答として返す。 ただし、 非同期実行。 できれば、 マルチスレッドで並列実行。

これは次のようなテストケースで表現できます。
※ ただし、 2CPU の PC で実行しています。 1CPU では並列実行してくれません。

【FeedReader: 並列実行のテスト】

まずは、 シンプルな実装を書いてみます。

【FeedReader: 並列処理の実装に挑戦(失敗!)】

これでは、 ループを回るごとに、 非同期の ReadAsync(url) を呼び出したところで制御を戻してしまい、 そのひとつの非同期処理が終わってから result.Add() し、 ようやく次のループに進みます。 つまり、 非同期で動いているので UI をフリーズはさせないけれど、 フィードの取得処理は並列実行していないのです。 5つの URL を処理するには、 5倍の時間が掛かってしまいます。

これの巧い解決法は、 ちょっと分かりませんでした。 不細工ですが、 次のようにループをふたつに分ければ、 一応目的を達成できました。 もっと綺麗な書き方があったら教えてください。 (2011/12/7追記: かなりマシな書き方が分かりました。 文末に追記。)

【FeedReader: 並列処理の実装】

タスク開始のループでは制御を返すことなく、 どんどんタスクを走らせています (実際に走っているかどうかはランタイム側の制御による)。 タスクから結果を取り出すループの方では、 各タスクごとに await して、 ループのつど制御を返しています。

20111205_vs11dp03

■ Task.Result では例外処理が面倒

最後に、 非同期実行中に発生した例外を、 結果取得時にキャッチする方法について。
「Task は Wait() しないとダメ?」 のところで、 Result プロパティじゃなくて GetAwaiter().GetResult() を使うコードを載せましたが、 その違いの確認です。

まず、 例外発生をシミュレートするために、 製品コードに仕掛けを入れます。

【FeedReader: 非同期実行中の例外をシミュレートする仕掛け】

#if DEBUG で囲ってありますから、 この仕掛けはリリースビルドには入りません。 引数に、 test__RaiseErrorUrl にセットした URL と同じものが渡されたときに、 test__RaiseException にセットされている例外を発生させる、 という仕掛けです。

では、 テストコードを書いて動作を確認してみましょう。

【FeedReader: 非同期実行中に発生した例外を捕まえるテスト】

task.Result では、 発生した例外が System.AggregateException にラップされて出てきます。 これでは catch 句で振り分けることができず、 不便です。
task.GetAwaiter().GetResult() だと、 発生した例外がそのまま出てきます。
GetAwaiter() は .NET Framework 4.5 から。

■ おわりに

WinRT で必須になる新しい非同期処理の手法を、 少しだけ齧ってみました。
async / await の威力が多少なりとも実感していただけたなら、 幸いです。

最後に、 副作用に無頓着な人は await の後に副作用を起こすコード書いてしまい、 たっぷりハマるんだろうなぁ~ と予言しておきましょう。 f(^^;

 


■ 追記: 並列処理の実装 (改良版)

(2011/12/7追記)
foreach が2つ並んでいて不細工だった並列処理の実装を改良しました。
・ コレクションに対する処理は LINQ 拡張だよね~♪
Task.WhenAll() って便利なものがあるじゃないか!!
WhenAll() は .NET Framework 4.5 から。 4.0 では WaitAll() してから取り出せばよい。

【FeedReader: 並列処理の実装 (改良版)】

|

« [本] アジャイルサムライ ~ アジャイルマインドの伝道書 | トップページ | [.NET Framework 4/4.5][C# 5] 非同期で動くビジネスロジックを作る »

プログラミング」カテゴリの記事

* プログラミング ( Metro スタイル )」カテゴリの記事

-プログラミング ( VS2012 )」カテゴリの記事

コメント

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック

この記事のトラックバックURL:
http://app.cocolog-nifty.com/t/trackback/209349/53421002

この記事へのトラックバック一覧です: [C# Advent Calendar 2011] Win8 に備えて async / await を勉強してみよう:

» [.NET Framework 4/4.5][C# 5] 非同期で動くビジネスロジックを作る [biac の それさえもおそらくは幸せな日々@nifty]
C# Advent Calendar 2011 参加記事 「Win8 に備えて async / await を勉強してみよう」 で書いたように、 async / await が使えれば非同期処理のコーディングがあっさり出来てしまいます。 たとえば今まで 3秒も掛かっていた UI のイベントハンドラーに async / ... [続きを読む]

受信: 2011年12月 9日 (金) 23時34分

« [本] アジャイルサムライ ~ アジャイルマインドの伝道書 | トップページ | [.NET Framework 4/4.5][C# 5] 非同期で動くビジネスロジックを作る »