こんにちは。 連休でだいぶ間が空いてしまいましたが、今週は、これからの Web 開発に備えて、.NET Web 開発における Async (Asynchronous) の意義を、ASP.NET MVC を例に簡単に説明します。(今後のセミナーなどでも頻繁にこの話に触れることになると思いますので、そのための準備として記載しておきます。) 補足 : 下記で紹介する async を使った非同期処理は、ASP.NET WebForm でも可能です。(ただし、記述方法が多少煩雑になります。) これについては、「ASP.NET WebForms 4.5, WCF 4.5 における非同期 (async) メソッド」の投稿に記載しました。 UI 系 (クライアント サイド) では、よく、UI スレッドを止めない手法として非同期を紹介しますが、サーバー サイドを含む Web 開発では、下記の通り、また違った尺度での考え方が必要となります。
Why async ? – Web 開発 (サーバー サイド) における非同期実装 (Async) の意義 まず、基本的な話からですみません。IIS / ASP.NET における処理では、以下の 3 つを理解しておきましょう。
ブラウザーなど、クライアント側からの Request (要求) は、いったん Queue に入り、処理の開始と共に Thread が割り当てられます。(Queue にも入りきらない場合には、503 エラーが返されます。) ASP.NET の場合は、ASP.NET のパイプラインで処理が開始されると、CLR スレッド プール (Thread Pool) から Managed Thread が割り当てられて、処理が実行されます。(.NET Framework の既定の設定では、Thread の上限なども、この CLR スレッド プールの管理に任せています。) こうした動きから想像できるように、Request、Queue、Thread の中で、特に Thread はオーバーヘッドも大きく、大量のトランザクションを処理する際、全体のスループット (throughput) に大きく影響します。(Request の上限値も最近では非常に大きな値 (既定値) が割り当てられており、あまり気にならないはずです。) 補足 : なお、以降の確認は Windows Server を使用してください。(Windows 7 など Client 環境では、そもそも Concurrent 接続数が最大 10 に制限されています。) まず、準備として、ASP.NET の既定値を変更しておきましょう。ここで使用する ASP.NET 4.5 の既定値は、1 論理コア当たり 100 スレッドまで使用可能になっていますから、このあとの実験がやりやすいように、この値を減らします。 補足 : .NET Framework 4, .NET Framework 4.5 (Beta) などの既定値では、processModel として autoconfig=”true” が設定されています。この autoconfig は、こちら の推奨に従って、1 論理コア当たり Max 100 スレッド (最大です) の設定をおこないます。 補足 : Windows Azure の Cloud Services で確認する場合は、Startup Task を使って ApplicationPool の構成を変更してください。(appcmd.exe を使用) . . . 上記では、CPU ごとの最大ワーカースレッド数を設定しています。このため、私の環境 (論理 2 コア) では、今回、Worker Theread が最大 10 個まで使用可能です。(よって、以降では、その前提で話を続けます。Web Garden とせず、Worker Process も 1 個という前提で進めます。) では、さっそく実験をしてみましょう。例えば、MVC のコントローラーで Timer を使って処理をおこなう場合、通常の ASP.NET MVC アプリケーションでは以下のようになるでしょう。(コントローラーの処理自体はシングル スレッドのため、下記の通り、待機と同期のためのコードが必要です。) . . .using System.Threading;. . .public class Sample1Controller : Controller{ public ActionResult Test1() { Guid id = Guid.NewGuid(); Log(id, 'start'); AutoResetEvent auto = new AutoResetEvent(false); Timer timer = new Timer(new TimerCallback((s) => { AutoResetEvent a = (AutoResetEvent)s; a.Set(); }), auto, 30000, Timeout.Infinite); auto.WaitOne(); Log(id, 'end'); return View('Index'); } // Write log (sorry, not using trace ...) private void Log(Guid id, string op) { while (true) { try { using (System.IO.FileStream fs = new System.IO.FileStream(@'C:\Demo\test.log', System.IO.FileMode.Append, System.IO.FileAccess.Write, System.IO.FileShare.None)) using (System.IO.StreamWriter wr = new System.IO.StreamWriter(fs)) { wr.WriteLine('{0} {1} {2}', DateTime.Now.ToLongTimeString(), id.ToString(), op); } } catch (System.IO.IOException) { System.Threading.Thread.Sleep(500); continue; } break; } }}. . . この Web アプリケーションに、Visual Studio のロードテスト (Load Test) などを使用して、多数の要求 (Request) を同時に投げてみてください。 まず、同時 9 接続でテストをおこなうと、9 つの要求 (Request) が Managed Thread (IIS の worker thread) で処理されて、30 秒後には処理が終了します。そして、期待通り、次の要求が正しく処理されます。 補足 : worker process で処理されている要求 (Request) の内容は、IIS 管理マネージャーの [ワーカー プロセス] で確認できます。下図の赤で囲った部分が、Managed thread (worker thread) で処理されている要求 (Request) です。 補足 : なお、あまりお勧めはしませんが、Thread Pool を使用しない場合は、この制約は受けません。(リソースの使用可能な範囲で、いくつでもスレッドの実行が可能です。) つぎに、上記の処理を、下記の通り AsyncController (確か、MVC 2 以降から追加されたやつ) を使って書き直してみましょう。 . . .using System.Threading;. . .public class Sample2Controller : AsyncController{ public void Test1Async() { AsyncManager.OutstandingOperations.Increment(); Guid id = Guid.NewGuid(); Log(id, 'start'); Timer timer = new Timer(new TimerCallback((s) => { AsyncManager.Parameters['id'] = id; AsyncManager.OutstandingOperations.Decrement(); }), null, 30000, Timeout.Infinite); } public ActionResult Test1Completed(Guid id) { Log(id, 'end'); return View(); } // Write log (sorry, not using trace ...) private void Log(Guid id, string op) { while (true) { try { using (System.IO.FileStream fs = new System.IO.FileStream(@'C:\Demo\test.log', System.IO.FileMode.Append, System.IO.FileAccess.Write, System.IO.FileShare.None)) using (System.IO.StreamWriter wr = new System.IO.StreamWriter(fs)) { wr.WriteLine('{0} {1} {2}', DateTime.Now.ToLongTimeString(), id.ToString(), op); } } catch (System.IO.IOException) { System.Threading.Thread.Sleep(500); continue; } break; } }}. . . 注意 (2012/12/12 追記) : このコードは、Windows Azure 上では使用しないでください。Windows Azure の WebRole で、こうした Timer 処理をおこなう場合は、「Windows Azure の WebRole で効率的な Timer 処理をする」を参照してください。 このコードの場合、実行結果は大きく異なり、同時 10 接続はもちろん、さらに大量の要求 (Request) も同時に処理できます。その理由 (背景) を考察してみましょう。 まず、標準の Controller (前述の Sample1Controller) の場合には開始から終了までの処理を同一のスレッドで処理します。(これは、いたって一般的なパターンです。) 一方、上記の AsyncController の場合には、Timer の待機時 (上記の Test1Async メソッドの終了後) にスレッドを解放します。(正確には、スレッド プールに戻します。) そして、Timer callback 時に、再度、使用可能なスレッドがスレッド プールから割り当てられます。つまり、上記コードの Log(id, “start”) と Log(id, “end”) の箇所では、ぞれぞれ別のスレッドが割り当てられます。(たまたま同じになる場合もありますけど。。。) このため、上記の通り、Managed Thread を 10 個に制限しても、30 秒の待機の間はスレッドは使用 (もしくは、ブロック) されておらず、空いているスレッドを使って他の要求 (Requset) を同時に処理できるようになります。 この動きを、スライド (アニメーション) にしてみました。(下図を Click してください) 何でもかんでも非同期 (asynchronous) を使いたくなるかもしれませんが、こうした動きのため、扱い方に注意してください。 補足 : Monitoring の目的で、ワーカー スレッドの最大数 (maxWorkerThreads) や使用可能なワーカー スレッド数などの情報を取得する場合は、下記のコードで取得できます。 なお、ここでは動きを理解する目的で上記の通り maxWorkerThreads を設定しましたが、前述の通り、既定では、充分大きな値が設定されていますので安心してください。(ほどほどのトランザクションなら、めったに枯渇するようなものではありません。) 設定値については、下記を参照してください。 [MSDN] processModel Element (.NET Framework 4) 補足 : ここでは説明を省略しますが、使用する IO thread (Completion Port thread) も、上記の worker thread 同様に管理?設定できます。(例えば、IO thread を制限するには、上記の machine.config で、maxIoThreads を設定します。) 補足 : HttpListener を使用した Selfhost (ASP.NET Web API の HttpSelfHostServer など ) や WCF では、worker thread の代わりに Thread pool の IO thread が使用されます。(同じ理屈で、非同期処理にすることで、スレッドの無駄使いを節約できます。)
これからの .NET Web 開発 ~ Async, Async, Async ! 上記のコードでは、AsyncController、AsyncManager などを作って、非同期を意識した特別な書き方をおこないました。 まず、ASP.NET MVC 4 では、.NET で登場した Task を使って、上記と同じ効率的な処理を以下のように記述できます。(下記の通り、AsyncController を使用する必要はありません。) . . .using System.Threading.Tasks;using System.Net.Http;. . .public class Sample3Controller : Controller{ public Task 上記では、コンテンツの取得に時間のかかるページ (http://testsite/HeavyWeb/) を取得していますが、この結果の取得を待機する際、Step 1 (上記のコメント) で割り当てられたスレッドは、いったんスレッド プールに戻されます。そして、Step 2 (上記のコメント) で、新しいスレッドがスレッド プールから割り当てられます。 さらに、今後正式リリースする ASP.NET 4.5 (.NET Framework 4.5 の ASP.NET) を使うと、同じ処理は、async、await キーワードによって、以下の通り記述できます。(Async CTP でも、この async / await が使用できます。) . . .using System.Threading.Tasks;using System.Net.Http;. . .public class Sample4Controller : Controller{ public async Task 上記の async, wait はシンタックス上のキーワード (「言語」が持つ機能です) で、コンパイル時に、前述したコード (Sample3Controller) と同等の処理に変換されます。 補足 : 実際には、上記の Sample3Controller とはまったく別のコードに変換されますので注意してください。(“同等” の処理に変換されますが、使用されるクラスや処理などは全く異なります。また、スレッドの使用方法も、ContinueWith を使用した場合と異なります。) ここでは、コンパイラー サービスの処理についての説明は省略します。 補足 : データベース接続 (IO) の Task ベース (TAP) の非同期呼び出しについて : 補足 : Task ベース (TAP) に対応していないリソースへのアクセスについて : 比較のために、Async ではなく、ごく普通に Sync で書いた場合には下記のコードになります。大変似たコードになっているのがお分かり頂けるでしょう。(上記と下記のコードは似ていますが、スレッド利用の観点では、まったく動作が異なります。下記では、GetHeavyPage() の呼び出し前後を含め、ずっと同じワーカー スレッドを占有するため、要求数が増えた場合、すぐにワーカー スレッドがパンクすることでしょう。) public ActionResult Test1(){ HttpResponseMessage res = GetHeavyPage().Result; ViewBag.ResultHeaders = res.ToString(); return View();}
ということで、ここではベース (導入部分) を紹介しましたが、ご存知の通り、ASP.NET Web API、WCF 4.5 のクライアント proxy 等々、今後の .NET 開発では、さまざまな場面で非同期処理が利用可能になっていますので、是非、うまく乗りこなしてください。
|
|