++C++; // 未確認飛行 C ブログ

http://ufcpp.net/

今月は MSDN が非同期処理の記事だらけ

leave a comment »

非同期だらけだった BUILD の後というのもあって、MSDN Blog も MSDN Magazine も非同期だらけですねぇ、ほんと。

というか、BUILD のセッション内容がそのまま記事になった感じ。

いくつかの記事に関して、概要をメモ書き程度に:

Easier Asynchronous Programming with the New Visual Studio Async CTP

http://msdn.microsoft.com/en-us/magazine/hh456401.aspx

そもそも async が必要とされる理由に関して。応答性良くするために他にどういう手段が考えられて、その手段だと何が問題かを説明。

  • 解1: スレッドたくさん作る
    • 確かに応答性は改善
    • リソース無駄食い
      • (ほとんどは待ち時間なのに)客の数だけウェイター雇い、オーダーごとに2名ずつコック雇うような無駄遣い
  • 解2: DoEvents はさむ
    • これが解になりえるのは、小さい処理を大量にループする場合のみ
    • ループの合間に挟めない場合、無意味
      • (非同期 I/O だとそもそも絶望だし、ループであってもソースコードに手を入れれるとは限らない)
    • 最後のイベントから順にしかこなせない
      • 割り込みが入り続けたら、最初のイベント処理がいつまでたっても終わらない
    • 再帰的な再入(処理が終わる前に、同じ処理が呼び出される)される
      • この手の歳入があった時に、きちんと排他処理できる設計をする開発者あまりいない
  • 解3: 「これが終わったらこの処理を続ける」チェックリストを残して処理を中断
    • 旧来的には、コールバック(前段の処理が終わったことを通知)の登録
    • 元の同期版(ワークフロー図通りのコード)と比べてコードの見た目がぐちゃぐちゃに
      • 順序通りにかけなかったり、かけても再帰が深くなったり
      • 書くのも保守も大変 → 応答性というものに対して払うコストが高すぎる
  • 解4: そこで、C#/VB vNext の async
    • クラス ライブラリ(Task<T>)とコンパイラーががんばる
    • コールバック的な効率のいい非同期処理を、同期の場合と同じように書ける

Async Performance: Understanding the Costs of Async and Await

http://msdn.microsoft.com/en-us/magazine/hh456402.aspx

C#/VB の async を使えば、同期版のコードほぼそのままで非同期処理が書ける。もちろん、その分、多少のコストもかかるものの、ほとんどの場合は気にならないレベルのはず。

ただ、1歩進んだ視点としては、そのコストを気にかけ、必要に応じて(コストが見えるレベルになったとき)コストを避けれるというのも大事。この記事では、async で何も考えずに書いてて掛かる/やりようによっては避けれるコストについて説明。

  • Task をキャッシュしておく
    • Task のインスタンスの割り当てもそれなりにコスト
    • Task が完了してたら、Task.Result にすでに結果が入っているはずなので使いまわす
    • こういう場合、async キーワード使わずに、自前実装にして Task が自動生成されるのを避けたり
  • 同期コンテキストを意識する
    • await 読んだ時点で現在の同期コンテキスト(SynchronizationContext.Current)がキャプチャされる
    • その同期コンテキストを介して継続処理が呼ばれる
    • 結果として、意図せず、コンテキストの切り替え(多くの場合、スレッド間のマーシャリングにつながる)が増えることが
      • これを避けるための、ConfigureAwait っていう拡張メソッドも用意されてる
  • ガベージ コレクションよけ
    • async メソッドの中身は、内部的に一段階、ラムダ式でラップされる
    • なので、ただのローカル変数だと思っていたものが、匿名クラスのフィールドになったりする
    • 不用意にローカル変数増やすと、意図せずインスタンスを保持しっぱなしになって、ガベコレで回収されるのを遅らせる可能性がある
    • といっても、Sum(await a, await b, await c); みたいなことしても、どの道、一時変数が作られるんで、この場合は var ra = await a; で OK
  • 複数の Task を併走させる場合は WhenAll で await を1個に
    • 並列処理になる分、待ち時間減るはず

※同期コンテキスト

例えば以下のように、ある種の文脈(context)を持ってるフレームワークがある

  • WPF では、UI 操作は必ず UI Thread で行う必要がある
  • ASP.NET では、HTTP リクエストと処理が紐づいてる(紐づけが切れると正しくレスポンス返せない)

SyncronizationContext の Post メソッドとかで、必要な文脈で処理を行うことができる

  • WPF だと、DispatcherSynchronizationContex ってクラスがあって、ディスパッチャーを介して UI スレッドに処理を戻してくれる
  • ASP.NET だと、AspNetSynchronizationContext ってクラスがあって、ちゃんと HTTP リクエストとの紐づけを行ってくれる

Pause and Play with Await

http://msdn.microsoft.com/en-us/magazine/hh456403.aspx

async の内部実装について。

だいたい、「非同期メソッドの内部実装」で書いてるような内容。

Thinkin’ About Async

http://msdn.microsoft.com/en-us/magazine/hh456392.aspx

今月の MSDN Magazine は Editor’s Note すらも Async。

上記「Pause and Play with Await」の著者 Mads Torgersen と、Visual Studio のプログラム マネージャー Lisa Feigenbaum のインタビューの断片に。

インタビューのもっと詳細なのは、以下のブログで公開されていたり

内容を、いくつか断片的に拾うと

  • Q. 今から(すでに RTM な .NET 4 までの範囲で)できることは?
    • 非同期処理は全部 Task-based (Task を戻り値とするメソッド)で書いとけ
  • Q. C#/VB、いろいろ取り込みすぎでは?
    • 全部、C#/VB に適した形で取り込んでいる
    • だから、同じ機能でも C# と VB でずいぶん形が違う(たとえばラムダ式)
  • Q. async はもっと早くに提供されていてもよかったのでは?
    • 言語の設計は非常に慎重なプロセスで、正当性の確認が終わるまで新しい要素は入れない
  • Q. この新しい機能を広めていくのにどういう姿勢で臨みます?
    • もちろん、いろんな形態(サンプル、プレゼン、ハンズオン、フォーマル等々)で情報提供していく
    • 一方、言語設計のやり方にも影響があって、既存ユーザーを意識して製品作ってる(覚えやすく/受け入れやすくなるよう設計)

Task Exception Handling in .NET 4.5

http://blogs.msdn.com/b/pfxteam/archive/2011/09/28/10217876.aspx

互換性が最優先なんで、Task の挙動も大部分は .NET 4 から変わっていないものの、一部、例外の挙動とかで留意すべき点が。

Unobserved Exception

Task は、別スレッドで起こった例外を、元スレッドに伝搬する前提なので、別スレッド中で例外がハンドルされなくてもアプリはクラッシュしない(だから、Unhandled Exception という表記でなく、Unobserved Exception になった)。

Task.Wait() や Task.Result、もしくは、Error プロパティで例外を「観測」できる。一切観測されなかった場合に限り、Task の Finalize 時に Unobserved Exception を出して、アプリがクラッシュするという挙動。

ところでが、C# 5.0 で async が入るとまたちょっとややこしく。例えば、

Task op1 = FooAsync();
Task op2 = BarAsync();
await op1;
await op2;

というコードで、op1, op2 が両方例外を出した場合、 await op1 の時点で op1 の例外は観測されるものの、await op2 が実行されなくなって、こっちの例外が観測されず、アプリがクラッシュすることに。

そこで、Unobserved Exception でアプリをクラッシュさせるかどうかを選択できるオプションがついたとか(デフォルトだとクラッシュしない方に変更されてしまったので、テスト時にはクラッシュするようにオプションを変えること推奨)。

Task.Result と await task

Task は Parallel クラスとかでも使いまくってて、並列処理前提(複数のスレッドで起きた例外をまとめる必要がある)。あと、スタック トレースをどうするかという問題もあるので、どんな例外だろうと、別スレッド側で起こった例外を別の例外でラップしてから元スレッドに伝搬させる必要がある。

なので、Task.Result したときに、例外があった場合、AggreageException が throw されてた。

ところが、await task の場合だと、並列性考える必要ほとんどない(WhenAll で並列 await できるけども、それも利用度合いは2割にみたないはず)。スタック トレースの問題も、System.Runtime.ExceptionServices.ExceptionDispatchInfo ってのを導入したんで解消した。

なので、実は、AggregateExpcetion でラップしちゃう挙動、変えてしまいたい。さもないと、どんな例外だろうと catch (AggregateExcetion) になってしまって、無差別 catch になってしまう。

で、実際ちゃんと、await task はラップ前の例外をそのまま throw するようになっているけども、それは“Awaiter”の層でやってる。task.Wait() の代わりに、task.GetAwaiter().GetResult() とやれば、ラップ前の例外を受け取れるようになる。

おまけ

Task クラスって、IDisposable で、MSDN にも「Dispose しろ」と書かれてるのに、Dispose 呼んでる例を見たことがないなーと。

MSDN Forum で、「どうやって Dispose すればいいの?ネスト深くなると大変だけど」みたいな質問に対して、回答で、「イベント ハンドル(小さいネイティブ リソース)握ってるだけ(しかも、Task の作り方によっては使わない)なんで、そこまで気にすることない。楽にできる範囲で Dispose すればいい。」とかついてた。

Written by ufcpp

2011年10月19日 @ 07:15

カテゴリー: .NET, C#

コメントを残す