[C# Advent Calendar 2011] Win8 に備えて async / await を勉強してみよう
これは、 C# Advent Calendar 2011 の 12月 6日の記事です。
この記事では、 C# の新機能である async / await を解説します。
■ Windows 8 の Metro スタイル
この画像は、 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 に記述し、 バイナリーに組み込まれます。
※ 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) でも利用可能です。
■ 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 は使えない】
※ 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 して、 ループのつど制御を返しています。
■ 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 / Win8.1 ストアアプリ】 HttpClient で TLS 1.1 / 1.2 に対応するには(2018.06.17)
- 【VS2017 15.7pv2】 XAML のランタイム ツールに 「ヒートマップ」 が増えた(2018.03.28)
- 【.NET Core】 プロジェクトを作ると 「project.assets.json が見つかりません」 エラー(2018.02.10)
- 【#UWP】 ビットマップの表示色を変える (Win2D.uwp 経由で Direct2D を使う)(2017.08.23)
- 【#UWP】 CompactOverlay モード: Picture in Picture というか、「最前面に表示」するウィンドウを作る(2017.08.16)
「-プログラミング ( VS2012 )」カテゴリの記事
- Visual Studio/.NET Framework/WinRT のバグレポートを Visual Studio から提出して楽をする #vs2012 #vs2013(2014.03.31)
- Visual Studio/.NET Framework/WinRT のバグレポートを MS Connect に提出する #vs2012 #vs2013(2014.03.30)
- VS2012 Update2 の落とし穴 ~ BCL.Async と PCL [⇒解決](2013.04.15)
- Windows 8 RTM が MSDN で公開 ~ さぁ、 Metro アプリを作ろう!(2012.08.17)
- #cod2012jp 名古屋: Metro スタイルアプリ「くじ庵」制作記(2012.06.11)
「* プログラミング ( Metro スタイル )」カテゴリの記事
- 【.NET / Win8.1 ストアアプリ】 HttpClient で TLS 1.1 / 1.2 に対応するには(2018.06.17)
- 【VS2017 15.7pv2】 XAML のランタイム ツールに 「ヒートマップ」 が増えた(2018.03.28)
- 【#UWP】 ビットマップの表示色を変える (Win2D.uwp 経由で Direct2D を使う)(2017.08.23)
- 【#UWP】 CompactOverlay モード: Picture in Picture というか、「最前面に表示」するウィンドウを作る(2017.08.16)
- 【#UWP】 15063用の Acrylic Effect を、ちゃんと実装してみる(2017.08.05)
この記事へのコメントは終了しました。
コメント