非同期プログラミング

ASP.NET の非同期/待機の概要

Stephen Cleary

非同期/待機を取り上げているオンライン資料のほとんどは、クライアント アプリケーションの開発を前提としていますが、非同期をサーバーで活用できる場面はないのでしょうか。もちろん、あります。今回は、ASP.NET の非同期要求について全体的な考え方を説明します。また、非常に役立つオンラインの参考資料も紹介します。ここでは async や await の構文については触れません。構文については、初心者向けのブログ記事 (bit.ly/19IkogW、英語) や、非同期のベスト プラクティスについてのコラム (msdn.microsoft.com/magazine/jj991977) で既に取り上げています。今回は、特に ASP.NET での非同期のしくみに注目します。

Windows ストア アプリ、Windows デスクトップ アプリ、Windows Phone アプリなど、クライアント アプリケーションの場合、非同期に実行する最大のメリットは応答性です。この種のアプリでは、主に UI の応答性を確保するために非同期操作が使われます。サーバー アプリケーションの場合、非同期に実行する最大のメリットはスケーラビリティです。Node.js のスケーラビリティで鍵になるのは、本来備わっている非同期性です。Open Web Interface for .NET (OWIN) は非同期対応を前提に基礎から設計されています。また、ASP.NET も非同期対応にすることができます。非同期は、UI アプリだけのものではありません。

同期と非同期の要求処理の比較

非同期要求ハンドラーの説明に入る前に、ASP.NET の同期要求ハンドラーのしくみを簡単に確認しておきます。ここでは、システムの要求が、データベースや Web API などの外部リソースを利用する例を基に考えてみましょう。要求が送られてくると、ASP.NET はスレッド プールのスレッドの 1 つをその要求に割り当てます。要求ハンドラーは同期方式で作成されるため、同期をとって外部リソースを呼び出します。その結果、外部リソースの呼び出しから返るまで、要求スレッドはブロックされます。図 1 は、2 つのスレッドが含まれるスレッド プールで、そのうちの 1 つは外部リソースを待機しているためにブロックされています。

外部リソースを同期方式で待機
図 1 外部リソースを同期方式で待機

やがて外部リソースへの呼び出しから戻り、要求スレッドは要求の処理を再開します。要求が完了し、応答を送信できる状態になると、要求スレッドはスレッド プールに戻されます。

この処理でまったく問題ありませんが、ASP.NET サーバーが、処理スレッドを上回る数の要求を受け取ると、そうは行きません。そのような場合、外部要求はスレッドが空くまで実行を待機する必要があります。図 2 は、先ほどと同じ 2 スレッドのサーバーが、3 つの要求を受け取ったようすを示しています。

3 つの要求を受け取った 2 スレッドのサーバ
図 2 3 つの要求を受け取った 2 スレッドのサーバー

この場合、最初の 2 つの要求は、スレッド プールのスレッドに割り当てられます。どちらの要求も外部リソースを呼び出し、それぞれに割り当てられたスレッドをブロックします。3 つ目の要求は、既にシステムに到着していますが、スレッドが空くまでは処理を開始できません。3 つ目の要求のタイマーは作動を続け、HTTP エラー 503 (サービス利用不可) になる可能性があります。

しかし、少し考えてみてください。この 3 つ目の要求はスレッドを待機していますが、システムには現実にまったく処理を行っていないスレッドが 2 つ存在しています。この 2 つのスレッドは、外部呼び出しから戻るのを待って単にブロックされているにすぎず、実際の処理は何もしていません。実行状態でもなければ、CPU 時間も割り当てられていません。スレッドを必要とする要求が他に存在するのに、この 2 つのスレッドは無駄遣いされています。この状況に対処するのが非同期要求です。

非同期要求ハンドラーの動作はこれまでの説明とは異なります。要求が送られてくると、ASP.NET はスレッド プールのスレッドの 1 つをその要求に割り当てます。ただし今回は、要求ハンドラーが外部リソースを非同期に呼び出します。その結果、外部リソースの呼び出しから戻るまで、要求スレッドはスレッド プールに戻されます。図 3 は、2 つのスレッドが含まれるスレッド プールと、要求が外部リソースを非同期に待機するようすを表しています。

外部リソースを非同期に待機
図 3 外部リソースを非同期に待機

大きな違いは、非同期呼び出しの実行中に、要求スレッドがスレッド プールに戻される点です。スレッドがスレッド プールに戻されたら、その要求との関連はなくなります。非同期要求処理では、外部リソース呼び出しから戻った時点で、ASP.NET はスレッド プールのスレッドの 1 つをその要求に再び割り当てます。割り当てられたスレッドが、要求の処理を続行します。要求が完了すると、そのスレッドは再びスレッド プールに戻されます。つまり、同期ハンドラーでは要求の最初から最後まで同じスレッドが使われますが、非同期ハンドラーでは同じ要求に (タイミングによっては) 別のスレッドが使われる可能性があります。

これなら、3 つの要求が送られてきても、サーバーは難なく対処できます。要求が非同期処理を待機しているときは必ず、スレッドが解放されてスレッド プールに戻されるので、既存の要求だけでなく、新しい要求も処理できます。非同期要求なら、少ないスレッドで、多くの要求を処理できます。したがって、ASP.NET の非同期コードの最大のメリットは、スケーラビリティになります。

スレッド プールのサイズを増やせばよいのでは?

ここで必ず聞かれるのが、「スレッド プールのサイズを増やせばよいのではないか」という疑問です。そうしない理由は 2 つあります。スレッド プールのスレッドをブロックするよりも、非同期コードを使うほうが規模的にも速度的にもスケーラビリティが高くなります。

スレッドをブロックする場合と比べて、非同期コードでは使用されるメモリの量がはるかに少ないため、大きくスケール変換できます。最新の OS では、スレッド プールのスレッドごとに 1 MB のスタックとページング不可能なカーネル スタックが用意されています。大した量ではないように思えますが、サーバーには大量のスレッドがあることを考えてみてください。対照的に、非同期操作のメモリのオーバーヘッドはこれよりもはるかに少なくて済みます。したがって、非同期操作を使用している要求の方が、スレッドをブロックしている要求よりもメモリに対する負荷は格段に軽くなります。非同期コードなら、もっと別の処理 (キャッシュなど) にメモリを使用できます。

スレッド プールの挿入率には制限があるため、非同期コードは、ブロックするスレッドよりも短時間でスケール変換されます。本稿執筆時点の挿入率は 2 秒につき 1 スレッドです。この挿入率の制限があるおかげで、スレッドの生成と破棄がひっきりなしに行われずに済んでいます。ただし、突然大量の要求が送られてきた場合はどうなるでしょう。同期コードであれば、一部の要求が利用可能なスレッドを使い果たし、残りの要求はスレッド プールに新しいスレッドが挿入されるのを待たなくてはならないため、簡単にフリーズが発生します。一方、非同期コードにはこのような制限はなく、いわゆる "常にオン" の状態です。非同期コードは、要求量の突然の増加にも比較的すばやく対応できます。

ただし、非同期コードはスレッド プールに代わるものではありません。スレッド プールか非同期コードかのどちらかではなく、スレッド プールと非同期コードの両方が必要です。非同期コードによって、アプリケーションはスレッド プールを最も効率よく活用できるようになります。非同期コードによって、既存のスレッド プールの力が最大限に引き出されます。

非同期処理を行うスレッドはどうなるのか?

これもいつも聞かれる質問です。つまり、「外部リソースへの I/O 呼び出しをブロックしているスレッドがどこかにあるはずだ」という疑問です。「非同期コードは要求スレッドを解放しても、システム内のどこかにある別のスレッドが犠牲にならないと無理だよね」ということですが、実際はまったくそんなことはありません。

非同期要求のスケーラビリティが高い理由を理解できるように、非同期 I/O 呼び出しの (単純化した) 例を使って説明します。ある要求でファイルへの書き込みが必要だとします。要求スレッドは非同期書き込みのメソッドを呼び出します。WriteAsync は基本クラス ライブラリ (BCL) によって実装され、非同期 I/O には完了ポートを使用します。したがって、WriteAsync 呼び出しは非同期のファイル書き込みとして OS に渡されます。これを受け取った OS はドライバー スタックと通信し、書き込むデータを I/O 要求パケット (IRP) に格納して渡します。

ここからの処理がポイントです。デバイス ドライバーが IRP を直ちに処理できない場合、これを非同期に処理しなければなりません。そこで、ドライバーはディスクに書き込みを開始する指示を送り、OS には "保留" 応答を返します。OS はその "保留" 応答を BCL に渡し、BCL は完了していないタスクを要求処理コードに返します。要求処理コードはタスクを待機し、メソッドからは完了していないタスクが返される、というように処理が続きます。最終的に要求処理コードが完了していないタスクを ASP.NET に返し、要求スレッドが解放されてスレッド プールに返されます。

この時点のシステムの現在状態を考えてみましょう。割り当てられたさまざまな I/O 構造 (Task インスタンスや IRP など) がありますが、それらはすべて保留中/未完了の状態です。それでも、この書き込み操作の完了を待機するためにブロックされているスレッドはありません。ASP.NET にも、BCL にも、OS にも、デバイス ドライバーにも、この非同期操作用に専有しているスレッドはありません。

ディスクがデータの書き込みを完了すると、割り込みによってドライバーにその旨通知します。ドライバーは OS に IRP の完了を通知し、OS は完了ポートから BCL に通知します。スレッド プールのスレッドの 1 つがその通知に応答し、WriteAsync から返されたタスクを完了します。それを受けて、非同期要求コードの処理が再開されます。この完了通知フェーズ中は、わずかな時間 "借り出される" スレッドがいくつかありますが、書き込み処理の進行中に実際にブロックされていたスレッドはありません。

この例はかなり単純化していますが、重要なポイントはおわかりいただけると思います。つまり、本当に非同期の操作には、スレッドは必要ありません。実際にデータを書き出すために CPU 時間が必要になることはありません。他にも、ついでながら知っておくべきことがあります。デバイス ドライバーについて考えると、デバイス ドライバーは IRP を直ちに処理するか、非同期に処理する必要があります。同期処理は使用できません。デバイス ドライバーのレベルでは、細かい I/O 以外の I/O はすべて非同期です。多くの開発者は、I/O 操作にとって "自然な API" は同期操作で、非同期 API はこの自然な同期 API の上にレイヤーとして用意されているという思い込みがあります。しかし、これはまったく逆です。実際には、非同期こそ自然な API であり、同期 API が非同期 I/O を使って実装されています。

非同期ハンドラーが用意されてこなかったのはなぜ?

非同期要求処理がそれほどすばらしいものなら、なぜ用意されていないのでしょう。実は、非同期コードはスケーラビリティが非常に高いので、ASP.NET プラットフォームでは Microsoft .NET Framework が登場した当初から、非同期ハンドラーと非同期モジュールがサポートされています。非同期 Web ページは ASP.NET 2.0 で導入され、MVC の場合は ASP.NET MVC 2 に非同期コントローラーが導入されています。

ただし、最近になるまで、非同期コードは例外なく作成するのが面倒で、メンテナンスが困難でした。多くの企業は、全面的にコードを同期方式で開発する方が容易だと考え、大規模なサーバー ファームや高価なホスティングに費用をかけています。しかし、最近風向きが変わりました。ASP.NET 4.5 では、async と await を使った非同期コードは、同期コードとほぼ同じくらい簡単に作成できるようになりました。大型システムがクラウド ホスティングに移行し、さらにスケーラビリティが求められるようになっていることで、ASP.NET の async と await を利用する企業が増えています。

非同期コードは特効薬ではない

非同期要求処理は非常にすばらしいものですが、問題をすべて解決できるわけではありません。ASP.NET の async と await で実現できることについて、一般にいくつか誤解があります。

async と await を知った開発者の中には、これは、サーバー コードがクライアント (ブラウザーなど) に "メリットをもたらす" ための手段であると思う方がいます。しかし、ASP.NET の async と await が "メリットをもたらす" のは ASP.NET ランタイムだけです。HTTP プロトコルは何も変わらず、やはり 1 つの要求に対して 1 つの応答しか返されません。async/await を使う前に SignalR、AJAX、UpdatePanel のいずれかが必要だった場合は、async/await を取り入れた後もやはり SignalR、AJAX、UpdatePanel のいずれかが必要です。

async/await による非同期要求処理は、アプリケーションのスケール変換に有効ですが、それは 1 台のサーバー上でのスケール変換の話であり、スケール アウトの場合にはやはり計画が必要です。スケール アウト型のアーキテクチャが必要であれば、依然として、ステートレスでアイデムポテントな要求と、信頼性の高いキュー処理を検討する必要があります。async/await はある程度は役に立ちます。async/await を使うとサーバー リソースを最大限活用できるので、スケール アウトが必要になる頻度は減ります。しかし、実際にスケール アウトが必要な場合は、正しい分散アーキテクチャを用意する必要があります。

ASP.NET の async と await が役立つのは I/O 関連です。ファイルやデータベース レコード、REST API の読み取りと書き込みは本当に高速化されます。ただし、CPU を集中的に使用するタスクには適していません。Task.Run を待機することで、バックグラウンド作業を開始できますが、それをする意味はありません。実際は、ASP.NET スレッド プールのヒューリスティックに干渉するため、スケーラビリティが損なわれます。ASP.NET で CPU を集中的に使用する作業を行う場合は、単純に要求スレッドでその作業を直接実行するのが最適です。基本的に、ASP.NET ではスレッド プールのキューに作業を登録しないでください。

最後に、システム全体のスケーラビリティについて見て行きましょう。ひと昔前は、1 台の ASP.NET Web サーバーがバックエンドの SQL Server データベース 1 つと通信するのが一般的なアーキテクチャの 1 つでした。このようなシンプルなアーキテクチャでは、通常、Web サーバーではなく、データベース サーバーがスケーラビリティのボトルネックになります。データベース呼び出しを非同期にしても、おそらく役に立ちません。Web サーバーのスケール変換には確かに非同期処理は有効ですが、データベース サーバーのためにシステム全体のスケール変換は実現できません。

Rick Anderson の秀逸なブログ記事「Should My Database Calls Be Asynchronous?」(データベースベース呼び出しを非同期にすべきか) (bit.ly/1rw66UB、英語) では、非同期データベース呼び出しに異議が唱えられています。その根拠は 2 つあり、1 つは非同期コードは難しい (そのため、大型のサーバーを購入するよりも、開発者の作業時間の方が費用がかかる) ことで、2 つ目はバック エンド データベースがボトルネックの場合、Web サーバーをスケール変換してもほとんど意味がないことです。この 2 つの根拠はどちらも、このブログが書かれた当時はまったくの正論でしたが、時間の流れと共にその論拠は弱まってきています。まず、async と await を使うことで、非同期コードの記述が大幅に容易になっています。また、Web サイトのデータ バックエンドは、世の中がクラウド コンピューティングに移行するに伴い、スケール変換できるようになっています。Microsoft Azure SQL Database、NoSQL、その他の API など、最近のバックエンドは、単一の SQL Server よりも格段にスケール変換できるため、再び Web サーバーがボトルネックになりつつあります。このようなシナリオでは、async/await により ASP.NET をスケール変換することで、大きなメリットがもたらされます。

始める前に

最初に知っておく必要があるのは、async と await は ASP.NET 4.5 でしかサポートされないことです。Microsoft.Bcl.Async という、.NET Framework 4 で async と await を可能にする NuGet パッケージがありますが、これは正しく動作しないので、使用しないでください。その理由は、ASP.NET 自体が、async および await との親和性を高めるために、非同期要求処理を管理する方法を変更する必要があったためです。NuGet パッケージにはコンパイラが必要とする型がすべて揃っていますが、ASP.NET ランタイムはその修正プログラムがありません。回避策はないため、ASP.NET 4.5 以上を使う必要があります。

また、ASP.NET 4.5 ではサーバーの "互換モード" が導入されていることに注意してください。新しい ASP.NET 4.5 プロジェクトを作成する場合は心配ありませんが、既存のプロジェクトを ASP.NET 4.5 にアップグレードすると、一様に互換モードが有効になります。互換モードはすべて無効にすることをお勧めします。それには、web.config を編集して httpRuntime.targetFramework を 4.5 に設定します。この設定にしてアプリケーションが失敗する (さらに、その修正の時間を取りたくない) 場合は、appSetting キーの aspnet:UseTaskFriendlySynchronizationContext の値を "true" に設定して追加することで、少なくとも async/await を有効にできます。この appSetting キーは、httpRuntime.targetFramework が 4.5 に設定されている場合は不要です。Web 開発チームが、この新しい "互換モード" についてブログ記事 (bit.ly/1pbmnzK) (英語) で詳しく説明しています。ヒント: 動作がおかしい、または例外が発生していて、コール スタックに LegacyAspNetSynchronizationContext が含まれている場合、アプリケーションはこの互換モードで実行されています。LegacyAspNetSynchronizationContext は async と互換性がありません。ASP.NET 4.5 の標準の AspNetSynchronizationContext が必要です。

ASP.NET 4.5 では、どの ASP.NET 設定にも、非同期要求に適した既定値が用意されていますが、その他の設定で 2、3 変更が必要になる可能性がある設定があります。1 つは IIS の設定です。IIS/HTTP.sys のキューの制限 (アプリケーション プールを選択し、[詳細設定] の [キューの長さ]) を既定値の 1,000 から 5,000 に変更することを検討してください。もう 1 つは .NET ランタイムの設定で、ServicePointManager.DefaultConnectionLimit です。これは、既定ではコア数の 12 倍の値が設定されています。この DefaultConnectionLimit は、同じホストへの同時発信接続数を制限します。

要求の中止についての注意

ASP.NET には、要求を同期処理する際、(要求がタイムアウトになった場合などに) 要求を終了させる非常にシンプルなメカニズムがあります。それは、その要求のワーカー スレッドを中止することです。このメカニズムは、各要求が最初から最後まで同じワーカー スレッドを使用する同期処理では有効です。スレッドの中止は、AppDomain の長期の安定性を考えると好ましいことではないので、ASP.NET では既定で、アプリケーションを定期的にリサイクルしてクリーンアップします。

非同期要求の場合、ASP.NET は要求を中止する必要がある場合でもワーカー スレッドを中止しません。代わりに、CancellationToken を使用して要求をキャンセルします。非同期要求ハンドラーは、キャンセル トークンを受け付け、キャンセル処理を行います。最も新しいフレームワーク (Web API、MVC、SignalR など) は CancellationToken を直接作成して渡すので、パラメーターとして宣言するだけです。また、HttpRequest などの ASP.NET トークンに直接アクセスすることもできます。TimedOutToken は要求がタイムアウトしたときにキャンセルする CancellationToken の 1 つです。

アプリケーションがクラウドに移行されるにつれて、要求の中止の重要性は増しています。クラウド ベースのアプリケーションでは、外部サービスへの依存度が高くなり、処理にかかる時間が読めません。たとえば、よくあるパターンの 1 つが、指数バックオフを使用する外部要求を再試行することです。アプリケーションが複数のこのようなサービスに依存している場合、要求処理全体のタイムアウトの上限を適用することをお勧めします。

非同期サポートの現状

多くのライブラリは、async との互換性を確保するために更新されています。Entity Framework (EntityFramework NuGet パッケージ) にはバージョン 6 で async のサポートが追加されました。ただし、遅延読み込みが行われないように十分注意する必要があります。遅延読み込みは、必ず同期実行されるためです。HttpClient (Microsoft.Net.Http NuGet パッケージ) は、async を考慮して設計された最新の HTTP クライアントの 1 つで、外部の REST API の呼び出しに便利です。HttpWebRequest と WebClient に代わる、最新のクライアントです。Microsoft Azure ストレージ クライアント ライブラリ (WindowsAzure.Storage NuGet パッケージ) は、バージョン 2.1 で async のサポートを追加しています。

Web API や SignalR などの新しいフレームワークでは、async と await を完全にサポートします。特に Web API では、非同期サポートを軸にパイプライン全体が作成されていて、非同期コントローラーだけでなく、非同期フィルターや非同期ハンドラーも用意されています。Web API と SignalR では、"とにかく使えば、動く" という、非常に自然な形で async がサポートされています。

ただし、悲しいことに、現在 ASP.NET MVC では、async と await は部分的にしかサポートされません。基本的なサポートはあり、非同期コントローラー アクションとキャンセルは適切に機能します。ASP.NET Web サイトには、ASP.NET MVC で非同期コントローラー アクションを使用する方法についてのすばらしいチュートリアルがあります (bit.ly/1m1LXTx、英語)。これは MVC の非同期処理の入門として最も役立つ資料です。残念ながら、ASP.NET MVC は (現時点では) 非同期フィルター (bit.ly/1oAyHLc、英語) や子の非同期アクション (bit.ly/1px47RG、英語) をサポートしていません。

ASP.NET Web フォームは古いフレームワークですが、非同期と待機を適切にサポートします。この場合も、入門として最適なリソースは、ASP.NET Web サイトの Web フォームのチュートリアル (bit.ly/Ydho7W、英語) です。Web フォームの場合、非同期サポートは任意で有効にします。最初に Page.Async を true に設定し、次に PageAsyncTask を使用して目的のページに非同期処理を登録する (または、async void イベント ハンドラーを使用する) 必要があります。PageAsyncTask はキャンセルもサポートします。

カスタムの HTTP ハンドラーまたは HTTP モジュールがある場合、ASP.NET では、それらの非同期バージョンもサポートします。HTTP ハンドラーは HttpTaskAsyncHandler (bit.ly/1nWpWFj、英語) によりサポートされ、HTTP モジュールは EventHandlerTaskAsyncHelper (bit.ly/1m1Sn4O、英語) によりサポートされます。

本稿執筆時点では、ASP.NET チームは ASP.NET vNext という新しいプロジェクトに取り組んでいます。vNext では、パイプライン全体が既定で非同期になります。現在の計画は、MVC と Web API を組み合わせて、非同期/待機を完全にサポートする 1 つのフレームワーク (非同期フィルターと非同期表示コンポーネントを含む) を作成することです。SignalR など、他の非同期対応フレームワークは、当然 vNext が実装されるでしょう。非同期の時代が来るのは間違いありません。

セーフティ ネットを軽視しない

ASP.NET 4.5 では、アプリケーションに存在する非同期の問題検出に役立つ新しい "セーフティ ネット" が 2、3 導入されました。これらは既定で有効になっているので、そのまま有効にしておきます。

同期ハンドラーが非同期操作の実行を試みると、「非同期操作を開始することができません」というメッセージと共に InvalidOperationException が返されます。この例外が返される理由は、主に 2 つあります。1 つは、Web フォーム ページに非同期イベント ハンドラーがあっても、Page.Async が true に設定されていないことです。もう 1 つは、同期コードが async void メソッドを呼び出していることです。これは、async void を避ける理由の 1 つでもあります。

もう 1 つのセーフティ ネットは、非同期ハンドラーに関するものです。非同期ハンドラーが要求を完了したのに、ASP.NET が非同期操作が完了していないことを検出した場合、InvalidOperationException が「非同期操作が保留中の間に、非同期のモジュールとハンドラーが完了しました」というメッセージと共に返されます。これは、通常、非同期コードが async void メソッドを呼び出していることが原因ですが、イベント ベースの非同期パターン (EAP) コンポーネント (bit.ly/19VdUWu、英語) の使い方が不適切であることが原因の場合もあります。

この 2 つのセーフティ ネットを無効にできるオプションがあります。HttpContext.AllowAsyncDuringSyncStages です (これは web.config で設定することもできます)。インターネット上のいくつかのサイトでは、上記の例外が発生した場合は、この設定をするように示唆していますが、私は断固反対です。冗談抜きで、なぜそのようなことができるか、理解できません。セーフティ ネットを無効するというのは、恐ろしい考えです。そんなことができるとすれば、何か非常に高度な非同期のしくみ (私が試したことがないようなもの) がコードに既に組み込まれていて、あなたがマルチスレッドの天才である場合のみです。ですから、この記事をすべてお読みになって、「かんべんしてよ、初心者じゃないんだから」とお考えになるのであれば、セーフティ ネットを無効にしてもよいのかもしれません。そうでない方は、これは非常に危険なオプションなので、それがもたらす悪い影響を十分認識していない限り、無効にしないでください。

作業の開始

ようやくここで、async と await を実際に利用する方法の説明を始めます。辛抱強くお付き合いいただき、ありがとうございます。

まず、このコラムの「非同期コードは特効薬ではない」をお読みになり、ご自分のアーキテクチャに非同期/待機が役立つことを確認してください。次に、アプリケーションを ASP.NET 4.5 に更新して、互換モードを無効にします (この時点では、何もエラーにならないことを確認するために、このモードを使用するのは悪い考えではありません)。これで、正真正銘、非同期/待機操作に取り組めるようになります。

まず "枝葉" の部分にとりかかります。要求がどのように処理され、I/O ベースの操作、中でもネットワーク ベースの操作がどのように特定されるかを考えます。代表的な例は、他の Web サービスや API への、データベース クエリやコマンドや呼び出しです。最初にとりかかるものを 1 つ選び、非同期/待機を使ってその操作を実行する場合の最適な方法を調べます。組み込みの BCL 型の多くは、.NET Framework 4.5 で非同期に対応しています。たとえば、SmtpClient には SendMailAsync メソッドがあります。一部の型は、非同期対応のものに置き換えることができます。たとえば、HttpWebRequest と WebClient は HttpClient に置き換えられます。必要に応じてライブラリのバージョンをアップグレードします。たとえば、Entity Framework は EF6 で非同期メソッドが追加されています。

ただし、ライブラリの "非同期もどき" は使わないでください。非同期もどきは、コンポーネントに非同期対応 API が含まれていますが、単にスレッド プールのスレッド内の同期 API をラップすることで実装されているものです。これは ASP.NET のスケーラビリティについては逆効果です。非同期もどきの例の 1 つが、Newtonsoft JSON.NET です。それさえなければ、これはすばらしいライブラリなのですが。(偽の) 非同期版 API を呼び出して JSON をシリアル化するのはやめ、すなおに同期版 API を呼び出してください。紛らわしい非同期もどきに、BCL ファイル ストリームがあります。ファイル ストリームを開く場合、非同期アクセスにするには明示的に開く必要があります。そうでないと、非同期もどきが使われ、スレッド プールのスレッドがファイルの読み取りや書き込み時に同期式でブロックされます。

1 つの "枝葉" を選んだら、その API を呼び出すコードのメソッドから作業にかかります。そのメソッドを、await を使って非同期対応 API を呼び出す非同期メソッドにします。呼び出し先の API が CancellationToken をサポートする場合は、CancellationToken を受け取り API メソッドに渡せるようにします。

メソッドを非同期にするときは、必ず戻り値の型を変更してください。void は Task にし、void ではない型の T は Task<T> にします。すると今度は、そのメソッドのすべての呼び出し元を非同期対応にして、タスクの待機などを実行できるようにする必要があります。また、「タスク ベースの非同期パターン」 (https://msdn.microsoft.com/ja-jp/library/hh873175.aspx) の名前付け規則に従って、Async をメソッド名に追加します。

async/await パターンにより、コール スタックを "幹" に成長させます。幹では、コードは ASP.NET フレームワーク (MVC、Web フォーム、Web API) とインターフェイスを取ります。このコラムの「非同期サポートの現状」で紹介した該当するチュートリアルを参考にして、非同期コードとフレームワークを統合してください。

その際に、スレッド ローカルな状態がないか、注意してください。非同期要求によってスレッドが変わることがあるので、ThreadStaticAttribute、ThreadLocal<T>、スレッド データ スロット、CallContext.GetData/SetData などのスレッド ローカルな状態は、機能しません。可能であれば、これらは HttpContext.Items に置き換えます。または、変更不可のデータを CallContext.LogicalGetData/LogicalSetData に格納します。

ここで、私が便利だと思ったテクニックを紹介しておきます。コードを (一時的に) 複製して、垂直分割を作成します。このテクニックでは、同期メソッドを非同期に変更しません。同期メソッド全体をコピーして、コピーしたものを非同期メソッドにします。アプリケーションの大部分を同期メソッドを使うように維持しながら、一部のみ垂直分割で非同期のスライスを作成します。これは、概念実証として非同期を使ってみる場合や、アプリケーションの一部に負荷テストを実施して、システムがどのようにスケール変換されるか感覚をつかむ場合にとても便利です。1 要求 (または 1 ページ) だけ完全に非同期対応にし、アプリケーションのその他の部分は同期のままにしておくことも可能です。もちろん、すべてのメソッドのコピーを保持しておく必要はなく、いずれは、I/O 中心のコードはすべて非同期に対応するため、同期コードは削除できます。

まとめ

このコラムが、ASP.NET の非同期要求の基礎的な考え方を理解する一助になればさいわいです。非同期 (async) と待機 (await) を使うことで、サーバー リソースを最大限活用できる Web アプリケーション、サービス、API をかつてないほど作成しやすくなりました。非同期は非常に便利です。


Stephen Cleary はミシガン北部在住の夫、父親兼プログラマです。彼は、マルチスレッドと非同期プログラミングに 16 年間取り組み、最初の Community Technology Preview から Microsoft .NET Framework の非同期サポートを使ってきました。彼のホーム ページとブログは、stephencleary.com (英語) から利用できます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James McCaffrey に心より感謝いたします。