新時代の非同期

いまさらasyncっていうか、twitter経由ネタです。
id:ladybug:20110412 非同期呼び出しの名前 に書いていますが、.NET Framework では非同期の手法が世代交代しています。

  • .NET 1.0/1.1 の APM (Async Programming Model)
  • .NET 2.0 の EAP (Event-based Async Pattern)
  • .NET 3.0 の EAP 拡張(WPF 向け SynchronizationContext の追加)
  • .NET 4 の TPM (Task Programming Model)

そして、

  • .NET 5? の TAP (Task-based Async Pattern)

となるわけです。
詳しいことはここではおいといて、前回の記事を踏襲した形の説明だけに絞り込んで話を進めると、

  • TAP は EAP のイベント処理を TPM で置き換えたもの

です。TAP が EAPTPM を合体させたような名称になっていることからもわかりやすいですね。これに、言語拡張によって自然な記述で期待した動きを実践する、それが TAP が提供する新しい非同期プログラミングパターンとなります。

具体的に

それでは、EAP のシンコンを使った処理を復習しましょう。

private void Button1_Click(object sender, EventArgs e)
{
   // Logic を呼び出し、結果が出たら TextBox へ表示する
   logic.Completed += DisplayResult;
   logic.DoAsync();
}

private void DisplayResult(object sender, LogicCompletedEventArgs e)
{
   // ここは呼び出し元コンテキストなので、直接処理しても大丈夫
   this.TextBox.Text = e.Result;
}

前回の記述そのままです。匿名メソッドやラムダ式を使用することもできますが、処理の順序が前後するのであえて分離したままにしています。これが、TAP では

private async void Button1_Click(object sender, EventArgs e)
{
   // Logic を呼び出し、結果が出たら TextBox へ表示する
   this.TextBox.Text = await logic.DoAsync();
}

とかけるようになります。ちょっとわかりにくいので、ローカル変数を使って

private async void Button1_Click(object sender, EventArgs e)
{
   // Logic を呼び出し、結果が出たら TextBox へ表示する
   var text = await logic.DoAsync();
   this.TextBox.Text = text;
}

とすると良いでしょうか。1行目の部分が EAP における Button1_Click の処理で、2行目の部分が EAP の DisplayResult にあたります。すごーく自然に記述することができていますが、この3つはすべて SynchronizationContext を使用して同期コンテキストを保存し、別スレッドでロジックを実行してから、SynchronizationContext を使用して元のコンテキストに戻ってきて続きを実行しているわけです。
さて、「あれ? Task はどこにいった?」と思われるかもしれませんが、TAP の基本的な処理の記述のうち1つは async/await 構文の導入による EAP の隠蔽になります。上記の例ではイベントハンドラの登録と非同期メソッドの呼び出ししかしていないこともあって、非同期処理に対する制御が一切でてこないので TAP が Task によって提供している部分をまったく説明ができないのです。逆に言えば、TAP の async/await 構文は生の EAP も、Task という生の TPM もきっちり隠蔽してくれているのです。*1

最初に戻って

さて、twitter でどんな話からコレを書くきっかけになったのかというと、

private void test1()
{
    using (var x = new X())
    {
        foo();
        await bar();
        baz();
        await qux();
        quux();
    }
}

みたいなことをしたとき、x.Dispose() が「何時」「どこで」呼ばれるかという問題です。「どこで」については、すでに上のほうの説明が回答になっていますが、この場合は new X() を行った同期コンテキストで x.Dispose() が実行されることになります。
具体的な話だと、特別な同期コンテキストの実装がない限り、new X() を実行したスレッドで x.Dispose() されます…と、この記事を書くまで思っていました。いや、実際に .NET4 まではそのとおりです。これは既定の SynchronizationContext の実装がそのように作られているからです。もし、違うスレッドで呼び出されてしまうようなことがあってしまうと、最初に書いたような

private async void Button1_Click(object sender, EventArgs e)
{
   // Logic を呼び出し、結果が出たら TextBox へ表示する
   this.TextBox.Text = await logic.DoAsync();
}

という処理はできなくなります。これが可能であるのは、await による非同期処理の呼び出し元と、非同期処理から戻ってきた後のスレッドが共に UI スレッドであるからです。そして、WinForms でも WPF でも SynchronizationContext は必ず元のスレッドに戻るように実装されているから、このようなことが可能になっています。
さてさて、「…と、この記事を書くまで思っていました。いや、実際に .NET4 まではそのとおりです。」というからには、TAP (Async CTP の時点では…ですが) では、これを裏切る SynchronizationContext が追加されるということなのです。ずばり、ASP.NET 用の SynchronizationContext が追加されます。
新しい同期コンテキストは、ASP.NET 2.0 でサポートされた非同期リクエスト処理をカバーし、ASP.NET 環境で TAP(と、EAP)による非同期処理を実現することができるようになっています。ただ、Web の非同期処理の都合もあって await の前でアサインされていたスレッドと await の後でアサインされるスレッドが同一とは限らないような同期コンテキストとなっています。もちろん、既に書いたようなかんじで、違和感なく記述&実行できるような実装となっているので、使うだけなら何にも心配しないでオッケーです。
この、新しい SynchronizationContext が増えるんだ!ということが書いてみたかっただけなのでした…。まあ、async/await は ASP.NET では使えませんよーってのは残念に思う人が大量に出ちゃうのでがんばったんでしょうね^^

あっ

「何時」について書くの忘れてた…けど、こっちはもういいよね?
yield によるイテレータの try-finally と同じだと言えば、わかる人はわかりますね。

*1:同期コンテキスト外では TPM の隠蔽になり、Task 大活躍ですが…