10月のMSDN マガジンのEditor’s Note(編集長より)にこんなセリフがあります。
Adding significant new functionality to a programming language is never a trivial matter.Feigenbaum describes language design as a “very conservative process,” emphasizing that features are not added until they’re exactly right.
(プログラミング言語に重要な新機能を追加するというのは、決して些細な問題ではありません。Feigenbaum(Visual Studioのプログラム マネージャー)は言語設計というものを「非常に保守的なプロセス」と説明し、厳格に正しいといえるまで機能を追加しないという点を強調しています。)
今日のお題はそんな辺り。C# 5.0の非同期メソッド/await演算子を振り返りつつ、保守的なプロセスについてのお話です。
await演算子
昨日のブログで説明した通り、I/O待ちをするような処理は、非同期版のメソッドを使って、スレッドを作らずに待つことが重要です。
しかしそうなると、今までは、いわゆるコールバック地獄でした。昨日書いた例ではTaskクラスを使っていたわけですが、それのContinueWithメソッドだらけになります。そしてラムダ式だらけです。
昨日のコードを再掲しておきましょうか。
var tasks = Common.GetQueries(context, keys)
.Select(x => x.ExecuteAsync()
.ContinueWith(t => { lock (w) Common.Output(t.Result, w); })
).ToArray(); // ToArray を付けて、ここで全タスクを先に起動してしまう。
Task.WaitAll(tasks.ToArray());
ContinueWithが1個で済んでるだけまだマシ!完了待ちもWaitAllが1個だけですし!
まあ、結構大変なコードになりがちです。そこでC# 5.0。await演算子の出番です。初回に書いたコードですが、以下のように、同期処理と同じような構造で、非同期処理が書けます。
var html = await c.DownloadStringTaskAsync(uri);
// 続きの処理…
早く!リリースはまだ!?
ということで、待望の機能です。C# 5.0のawait演算子。むちゃくちゃ重要。
なので、「もっと早く出せなかったの?」「Taskクラス(.NET 4での追加)と同時を期待してたけど」みたいな話も出てくるわけです。それに対する回答が、今回の冒頭の言葉です。
振り返れば長く
アイディア自体は古くから出ていたんですが、リリースまでは結構長い期間かけていますね。
まず、2005年前後(ちょうどC# 3.0の正式版が出た頃です)、.NETの方向性として「宣言的、動的、非同期」の3つが上がっていました。Taskクラスも、かなり早い時期からプレビュー版が公開されていました(後に、.NET 4で正式採用)。
そして、await演算子の原形となるアイディアは、確か、2009年ですかね。その年のPDC 2009だったと思うんですが、少しだけデモがありました。当時は、awaitではなく、yieldキーワードを使っていたはずです。それから、開発者が実際に触ってみれるもののプレビュー版が公開されたのは2010年10月です。
正式版は来年でしょうか、まだ未定ですが。来年だとして、7年くらい掛けてることになりますか。
実績ある機能の組み合わせ
実は、このawait演算子、C#という言語の構文上は、あまり冒険的なことはしていません。C# 2.0で導入されたイテレーター構文(yield return)と、.NET 4で導入されたTaskクラスの組み合わせでしかなく。実績ある機能を組み合わせたわかりやすい仕様です。
それでも、最初に公開されてから、いろいろな改善が入っています。.NET 4でいったんは正式リリースされたTaskクラスにも、かなり手が入るようです。
変更点
何にそんなに時間を掛けているのか、何が変わるのかが気になるところかと思います。
以下の3つの観点から説明していきましょう。
- ドッグフーディング
- 要件の変化
- ツールでのサポート
ドッグフーディング
ドッグフーディング(dogfooding)ってのは、自社製品を自社内で使い倒して洗練しろという意味です。
プログラミング言語に関していうと、言語機能を試すにあたって、まず第一に必要なのは対応するAPIです。となると、ドッグフーディングは、つまり、await演算子前提で作られた、マイクロソフト製の非同期APIをまず充実させろということになります。それが、WinRTですね。Windows 8では、非同期APIの利用が徹底されています。
非同期APIの徹底にはawaitが不可欠です。一方で、awaitという新機能の検証には、非同期APIの充実が不可欠です。ある意味、C# 5.0は、Windows 8と二人三脚で発展しているわけです。
要件の変化
Taskクラスが作られた当時、主な用途はParallelクラスを通した並列処理でした。CPUをフルに使う処理を並列化して高速化しよう、マルチ コアCPUをフルに活用しようというのが目的です。一方のawait演算子の目的は、I/O待ちの簡単化です。
要件が変わると、必要となる機能や最適化のポイントも変わってきます。
そのため、Taskクラスはかなりいろいろな修正を受けることになりました。.NET 4から.NET 4.5で、以下のような修正が入るそうです。
-
例外の伝搬
-
スレッド プール内で発生した例外を、呼び出し元に伝える方法が変わりました
- (Taskクラス自身の修正ではなく、1段ラップするそうなので、破壊的変更ではありません)
- .NET 4(Task.Wait): AggregateExceptionでラップする
- .NET 4.5(TaskAwaitor.OnComplete): ラップしない
-
-
未処理例外の扱い
-
スレッド プール内で発生した例外が、呼び出し元で処理されなかった場合の挙動が変わりました
- .NET 4: TaskクラスのFinalize時に未処理例外が再スローされ、アプリがクラッシュします
- .NET 4.5: デフォルトでは、上記の挙動がなくなりました(設定で変えれます)
-
-
リソースの分離
- Parallel(fork-joinスタイル)には必要だけども、await(継続スタイル)には必要ないリソースがあるので、それを分離、必要なときだけ確保するように変更
-
WaitAll/WaitAnyメソッドの改善
- 与えた複数のタスクにたいして、タスクごとに同期プリミティブを使っていたのを、全体でひとまとめにすることでオーバーヘッドを削減
-
Taskクラスのインスタンスをキャッシュしておけるように
- Task.Disposeメソッドが呼ばれた後でもResultの値を取得できるように変更
いずれも、並列処理からI/O待ちに焦点が移ったことに伴って重要になった部分の修正です。
その他にもいくつか、細かい性能調整が入っています。
参考:
-
- .NET 4から.NET 4.5での破壊的変更の一覧。Taskがらみ結構あります
- Task Exception Handling in .NET 4.5
- .NET 4.5におけるTask Parallel Libraryの改善
ツールでのサポート
さて、C#といえばVisual Studioです。そして、Visual Studioは、単なるコーディング ツールだけでなく、様々な機能を持っています。
非同期と関係するのは以下のような点です。
-
ステップ実行デバッグ
-
awaitした行の次はどこに飛ぶべきか
- 非同期処理を開始すると同時に、一度そのメソッドを抜けます
- 非同期処理完了後に、続きを再開します
- ステップ実行では、メソッドを抜けた先に行くべきか、再開後の続きに行くべきか
-
-
未処理例外
- 前述の通り、未処理例外があった場合、TaskクラスのFinalize時に例外が再スローされます
- デバッガーが大元の例外発生個所を表示してくれるような仕組みがないとデバッグしづらいです
-
単体テスト
- 非同期メソッドのテスト機構の追加が必要です
この辺り、プレビュー版ではまだ固まってない部分だったりします。
まとめ
プログラミング言語に新機能を追加するというのがどれだけ大変な作業か、その一端を垣間見ていただけたかと思います。
非同期処理というものは、ここ十年来のもっとも大きな流れですから、その変遷を追うといろいろ見えてくることも多いでしょう。
こんな記事のことを思い出しました。って、英語だからちゃんと読んではいないのですけど。
http://blogs.msdn.com/b/ericlippert/archive/2007/08/31/future-breaking-changes-part-two.aspx
確か、破壊的変更どころか、将来破壊的変更を引き起こしそうかどうかまで含めて検討しているという話ですねー。