この記事の対象: ✔️ .NET 9.0 以降のバージョン
このチュートリアルでは、ThreadPool の不足シナリオをデバッグする方法について説明します。 ThreadPool の不足は、プールに新しい作業項目を処理するための使用可能なスレッドがなく、多くの場合、アプリケーションの応答速度が低下する場合に発生します。 Core Web アプリ ASP.NET 提供されている例を使用すると、ThreadPool の枯渇を意図的に発生させ、診断する方法を学習できます。
このチュートリアルでは、次のことを行います。
- 要求に遅く応答しているアプリを調査する
- dotnet-counters ツールを使用して、ThreadPool の不足が発生している可能性を特定する
- dotnet-stack ツールと dotnet-trace ツールを使用して、ThreadPool スレッドをビジー状態に保つ作業を特定する
[前提条件]
このチュートリアルでは、次の内容を使用します。
- サンプル アプリをビルドして実行するための .NET 9 SDK
- スレッドプールの枯渇動作を実証するサンプル Web アプリ
- ボンバルディアはサンプル Web アプリの負荷を生成する
- パフォーマンス カウンターを監視する dotnet-counters
- dotnet-stack を 使用してスレッド スタックを調べる
- 待機イベントを収集する dotnet-trace
- 省略可能: 待機イベントを分析する PerfView
サンプル アプリを実行する
サンプル アプリのコードをダウンロードし、.NET SDK を使用して実行します。
E:\demo\DiagnosticScenarios>dotnet run
Using launch settings from E:\demo\DiagnosticScenarios\Properties\launchSettings.json...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: E:\demo\DiagnosticScenarios
Web ブラウザーを使用して要求を https://localhost:5001/api/diagscenario/taskwait
に送信すると、約 500 ミリ秒後に返された応答 success:taskwait
表示されます。 これは、Web サーバーが期待どおりにトラフィックを提供していることを示しています。
パフォーマンスの低下を観察する
デモ Web サーバーには、データベース要求を行い、ユーザーに応答を返すモックを作成する複数のエンドポイントがあります。 これらの各エンドポイントは、要求を一度に 1 つずつ処理するときに約 500 ミリ秒の遅延がありますが、Web サーバーに負荷がかかるとパフォーマンスが大幅に低下します。 Bombardier ロード テスト ツールをダウンロードし、125 件の同時要求が各エンドポイントに送信されたときの待機時間の違いを確認します。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait
Bombarding https://localhost:5001/api/diagscenario/taskwait for 10s using 125 connection(s)
[=============================================================================================] 10s
Done!
Statistics Avg Stdev Max
Reqs/sec 33.06 234.67 3313.54
Latency 3.48s 1.39s 10.79s
HTTP codes:
1xx - 0, 2xx - 454, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 75.37KB/s
この 2 番目のエンドポイントでは、さらに悪いパフォーマンスを発揮するコード パターンが使用されます。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait
Bombarding https://localhost:5001/api/diagscenario/tasksleepwait for 10s using 125 connection(s)
[=============================================================================================] 10s
Done!
Statistics Avg Stdev Max
Reqs/sec 1.61 35.25 788.91
Latency 15.42s 2.18s 18.30s
HTTP codes:
1xx - 0, 2xx - 140, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 36.57KB/s
どちらのエンドポイントも、負荷が高い場合の平均待機時間が 500 ミリ秒 (それぞれ 3.48 秒と 15.42 秒) を大幅に超えています。 以前のバージョンの .NET Core でこの例を実行すると、両方の例のパフォーマンスが同じように悪くなる可能性があります。 .NET 6 では、ThreadPool ヒューリスティックが更新され、最初の例で使用された不適切なコーディング パターンのパフォーマンスへの影響が軽減されました。
ThreadPool の不足を検出する
実際のサービスで上記の動作を観察した場合は、負荷がかかっている間に応答が遅いことがわかりますが、原因はわかりません。 dotnet-counters は、ライブ パフォーマンス カウンターを表示できるツールです。 これらのカウンターは、特定の問題に関する手がかりを提供することができ、多くの場合、取得が簡単です。 運用環境では、リモート監視ツールと Web ダッシュボードによって同様のカウンターが提供される場合があります。 dotnet-counters をインストールし、Web サービスの監視を開始します。
dotnet-counters monitor -n DiagnosticScenarios
Press p to pause, r to resume, q to quit.
Status: Running
Name Current Value
[System.Runtime]
dotnet.assembly.count ({assembly}) 115
dotnet.gc.collections ({collection})
gc.heap.generation
gen0 2
gen1 1
gen2 1
dotnet.gc.heap.total_allocated (By) 64,329,632
dotnet.gc.last_collection.heap.fragmentation.size (By)
gc.heap.generation
gen0 199,920
gen1 29,208
gen2 0
loh 32
poh 0
dotnet.gc.last_collection.heap.size (By)
gc.heap.generation
gen0 208,712
gen1 3,456,000
gen2 5,065,600
loh 98,384
poh 3,147,488
dotnet.gc.last_collection.memory.committed_size (By) 31,096,832
dotnet.gc.pause.time (s) 0.024
dotnet.jit.compilation.time (s) 1.285
dotnet.jit.compiled_il.size (By) 565,249
dotnet.jit.compiled_methods ({method}) 5,831
dotnet.monitor.lock_contentions ({contention}) 148
dotnet.process.cpu.count ({cpu}) 16
dotnet.process.cpu.time (s)
cpu.mode
system 2.156
user 2.734
dotnet.process.memory.working_set (By) 1.3217e+08
dotnet.thread_pool.queue.length ({work_item}) 0
dotnet.thread_pool.thread.count ({thread}) 0
dotnet.thread_pool.work_item.count ({work_item}) 32,267
dotnet.timer.count ({timer}) 0
上記のカウンターは、Web サーバーが要求を処理していない間の例です。
api/diagscenario/tasksleepwait
エンドポイントを使用してボンバルディアをもう一度実行し、負荷を 2 分間持続させ、パフォーマンス カウンターに何が起こるかを観察するのに十分な時間が必要です。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
ThreadPool の不足は、キューに登録された作業項目を処理する空きスレッドがなく、ランタイムが ThreadPool スレッドの数を増やすことで応答する場合に発生します。
dotnet.thread_pool.thread.count
値は、コンピューター上のプロセッサ コアの数の 2 ~ 3 倍に急速に増加し、125 を超えるどこかで安定するまで、1 秒あたり 1 ~ 2 個のスレッドが追加されます。 ThreadPool の不足が現在パフォーマンスのボトルネックであることを示す重要なシグナルは、ThreadPool スレッドと CPU 使用率が 100%よりもはるかに少ない低速で安定した増加です。 スレッド数の増加は、プールがスレッドの最大数に達するか、すべての受信作業項目を満たすのに十分なスレッドが作成されているか、CPU が飽和状態になるまで続行されます。 多くの場合、ThreadPool の不足は、 dotnet.thread_pool.queue.length
の値が大きく、 dotnet.thread_pool.work_item.count
の値が小さい場合にも表示されます。つまり、保留中の作業が大量にあり、完了する作業がほとんどありません。 スレッド数がまだ増加している間のカウンターの例を次に示します。
[System.Runtime]
dotnet.assembly.count ({assembly}) 115
dotnet.gc.collections ({collection})
gc.heap.generation
gen0 5
gen1 1
gen2 1
dotnet.gc.heap.total_allocated (By) 1.6947e+08
dotnet.gc.last_collection.heap.fragmentation.size (By)
gc.heap.generation
gen0 0
gen1 348,248
gen2 0
loh 32
poh 0
dotnet.gc.last_collection.heap.size (By)
gc.heap.generation
gen0 0
gen1 18,010,920
gen2 5,065,600
loh 98,384
poh 3,407,048
dotnet.gc.last_collection.memory.committed_size (By) 66,842,624
dotnet.gc.pause.time (s) 0.05
dotnet.jit.compilation.time (s) 1.317
dotnet.jit.compiled_il.size (By) 574,886
dotnet.jit.compiled_methods ({method}) 6,008
dotnet.monitor.lock_contentions ({contention}) 194
dotnet.process.cpu.count ({cpu}) 16
dotnet.process.cpu.time (s)
cpu.mode
system 4.953
user 6.266
dotnet.process.memory.working_set (By) 1.3217e+08
dotnet.thread_pool.queue.length ({work_item}) 0
dotnet.thread_pool.thread.count ({thread}) 133
dotnet.thread_pool.work_item.count ({work_item}) 71,188
dotnet.timer.count ({timer}) 124
ThreadPool スレッドの数が安定すると、プールは枯渇しなくなります。 ただし、高い値 (プロセッサ コアの数の約 3 倍以上) で安定した場合、通常はアプリケーション コードが一部の ThreadPool スレッドをブロックしており、ThreadPool はより多くのスレッドで実行することで補正されていることを示します。 スレッド数が多い場合に安定して実行すると、要求の待機時間に大きな影響を与えるわけではありませんが、時間の経過と同時に負荷が大幅に変化したり、アプリが定期的に再起動されたりする場合、ThreadPool が不足する期間に入り、スレッドがゆっくりと増加し、要求待ち時間が短くなる可能性があります。 各スレッドもメモリを消費するため、必要なスレッドの合計数を減らすと、もう 1 つの利点が得られます。
.NET 6 以降では、ThreadPool ヒューリスティックが変更され、特定のブロッキング タスク API に応答して ThreadPool スレッドの数が大幅に高速化されました。 これらの API では ThreadPool の不足が引き続き発生する可能性がありますが、ランタイムの応答速度が速いため、期間は以前の .NET バージョンよりもはるかに短くなります。
api/diagscenario/taskwait
エンドポイントで Bombardier をもう一度実行します。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
.NET 6 では、プールが以前よりも速くスレッド数を増やし、多数のスレッドで安定している必要があります。 スレッド数が増加している間、ThreadPool の不足が発生しています。
ThreadPool の不足を解決する
ThreadPool の不足を解消するには、ThreadPool スレッドをブロック解除したままにして、受信した作業項目を処理できるようにする必要があります。 各スレッドが何を行っていたかを判断するには、複数の方法があります。 問題がたまにのみ発生する場合は、 dotnet-trace を使用してトレースを収集することをお勧めします。一定期間にわたってアプリケーションの動作を記録することをお勧めします。 問題が絶えず発生する場合は、dotnet-stack ツールを使用するか、Visual Studio で表示できる dotnet-dump を使用してダンプをキャプチャできます。 dotnet-stack は、コンソールにすぐにスレッド スタックを表示するため、高速になる可能性があります。 ただし、Visual Studio のダンプ デバッグでは、フレームをソースにマップするより優れた視覚化が提供されます。Just My Code ではランタイム実装フレームを除外できます。並列スタック機能は、類似したスタックを持つ多数のスレッドをグループ化するのに役立ちます。 このチュートリアルでは、dotnet-stack オプションと dotnet-trace オプションを示します。 Visual Studio を使用してスレッド スタックを調査する例については、 ThreadPool の不足の診断に関するチュートリアル ビデオを参照してください。
dotnet-stack で継続的な問題を診断する
Bombardier をもう一度実行して、Web サーバーを負荷の下に置きます。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
次に、dotnet-stack を実行してスレッド スタック トレースを確認します。
dotnet-stack report -n DiagnosticScenarios
多数のスタックを含む長い出力が表示されます。その多くは次のようになります。
Thread (0x25968):
[Native Frames]
System.Private.CoreLib.il!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib.il!System.Threading.Tasks.Task.SpinThenBlockingWait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib.il!System.Threading.Tasks.Task.InternalWaitCore(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib.il!System.Threading.Tasks.Task`1[System.__Canon].GetResultCore(bool)
DiagnosticScenarios!testwebapi.Controllers.DiagScenarioController.TaskWait()
Anonymously Hosted DynamicMethods Assembly!dynamicClass.lambda_method1(pMT: 00007FF7A8CBF658,class System.Object,class System.Object[])
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+SyncObjectResultExecutor.Execute(class Microsoft.AspNetCore.Mvc.Infrastructure.IActionResultTypeMapper,class Microsoft.Extensions.Internal.ObjectMethodExecutor,class System.Object,class System.Object[])
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(value class State&,value class Scope&,class System.Object&,bool&)
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeAsync()
Microsoft.AspNetCore.Mvc.Core.il!Microsoft.AspNetCore.Mvc.Routing.ControllerRequestDelegateFactory+<>c__DisplayClass10_0.<CreateRequestDelegate>b__0(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.Routing.il!Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.Authorization.Policy.il!Microsoft.AspNetCore.Authorization.AuthorizationMiddleware+<Invoke>d__6.MoveNext()
System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start(!!0&)
Microsoft.AspNetCore.Authorization.Policy.il!Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.HttpsPolicy.il!Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.HttpsPolicy.il!Microsoft.AspNetCore.HttpsPolicy.HstsMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.HostFiltering.il!Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware.Invoke(class Microsoft.AspNetCore.Http.HttpContext)
Microsoft.AspNetCore.Server.Kestrel.Core.il!Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol+<ProcessRequests>d__223`1[System.__Canon].MoveNext()
System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.Threading.Tasks.VoidTaskResult,Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol+<ProcessRequests>d__223`1[System.__Canon]].MoveNext(class System.Threading.Thread)
System.Private.CoreLib.il!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(class System.Runtime.CompilerServices.IAsyncStateMachineBox,bool)
System.Private.CoreLib.il!System.Threading.Tasks.Task.RunContinuations(class System.Object)
System.IO.Pipelines.il!System.IO.Pipelines.StreamPipeReader+<<ReadAsync>g__Core|36_0>d.MoveNext()
System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.IO.Pipelines.ReadResult,System.IO.Pipelines.StreamPipeReader+<<ReadAsync>g__Core|36_0>d].MoveNext(class System.Threading.Thread)
System.Private.CoreLib.il!System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(class System.Runtime.CompilerServices.IAsyncStateMachineBox,bool)
System.Private.CoreLib.il!System.Threading.Tasks.Task.RunContinuations(class System.Object)
System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[System.Int32].SetExistingTaskResult(class System.Threading.Tasks.Task`1<!0>,!0)
System.Net.Security.il!System.Net.Security.SslStream+<ReadAsyncInternal>d__186`1[System.Net.Security.AsyncReadWriteAdapter].MoveNext()
System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
System.Private.CoreLib.il!System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[System.Int32,System.Net.Security.SslStream+<ReadAsyncInternal>d__186`1[System.Net.Security.AsyncReadWriteAdapter]].MoveNext(class System.Threading.Thread)
Microsoft.AspNetCore.Server.Kestrel.Core.il!Microsoft.AspNetCore.Server.Kestrel.Core.Internal.DuplexPipeStream+<ReadAsyncInternal>d__27.MoveNext()
System.Private.CoreLib.il!System.Threading.ExecutionContext.RunInternal(class System.Threading.ExecutionContext,class System.Threading.ContextCallback,class System.Object)
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
これらのスタックの下部にあるフレームは、これらのスレッドが ThreadPool スレッドであることを示します。
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
上部付近のフレームでは、DiagnosticScenarioController.TaskWait() 関数からの GetResultCore(bool)
の呼び出しでスレッドがブロックされていることが明らかになります。
Thread (0x25968):
[Native Frames]
System.Private.CoreLib.il!System.Threading.ManualResetEventSlim.Wait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib.il!System.Threading.Tasks.Task.SpinThenBlockingWait(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib.il!System.Threading.Tasks.Task.InternalWaitCore(int32,value class System.Threading.CancellationToken)
System.Private.CoreLib.il!System.Threading.Tasks.Task`1[System.__Canon].GetResultCore(bool)
DiagnosticScenarios!testwebapi.Controllers.DiagScenarioController.TaskWait()
dotnet-trace で断続的な問題を診断する
dotnet-stack アプローチは、すべての要求で発生する明確で一貫性のあるブロック操作に対してのみ有効です。 一部のシナリオでは、ブロックは数分ごとに散発的に行われるため、dotnet-stack は問題の診断に役立ちません。 この場合、dotnet-trace を使用して一定期間のイベントを収集し、後で分析できる nettrace ファイルに保存できます。
スレッド プールの不足を診断するのに役立つ特定のイベントが 1 つあります。WaitHandleWait イベントは、.NET 9 で導入されました。 スレッドが同期オーバー非同期呼び出し(Task.Result
、Task.Wait
、Task.GetAwaiter().GetResult()
など)や他のロック操作(lock
、Monitor.Enter
、ManualResetEventSlim.Wait
、SemaphoreSlim.Wait
など)によってブロックされるときに、発生します。
Bombardier をもう一度実行して、Web サーバーを負荷の下に置きます。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
次に、dotnet-trace を実行して待機イベントを収集します。
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
イベントを含む DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace
という名前のファイルが生成されます。 この nettrace は、次の 2 つの異なるツールを使用して分析できます。
- PerfView: Windows 専用の Microsoft によって開発されたパフォーマンス分析ツール。
- .NET イベント ビューアー: コミュニティによって開発された nettrace 分析 Blazor Web ツール。
次のセクションでは、各ツールを使用して nettrace ファイルを読み取る方法を示します。
Perfview を使用して nettrace を分析する
PerfView をダウンロードして実行します。
nettrace ファイルをダブルクリックして開きます。
高度なグループ>任意のスタックをダブルクリックします。 新しいウィンドウが開きます。
"Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start" 行をダブルクリックします。
これで、WaitHandleWait イベントが生成されたスタック トレースが表示されます。 "WaitSource" で分割されます。 現在、2 つのソースがあります。
MonitorWait
を介して生成されたイベントのと、他のすべてのイベントのUnknown
です。64.8% のイベントを表す MonitorWait から始めます。 チェック ボックスをオンにして、このイベントの出力を担当するスタック トレースを展開できます。
このスタックトレースは次のように読み取ることができます:
Task<T>.Result
が WaitHandleWait イベントを送信しました、そしてこのイベントは WaitSource MonitorWait を使用しています (Task<T>.Result
は待機を行うためにMonitor.Wait
を使用しています)。 これはあるラムダによって呼び出され、そのラムダはあるASP.NETコードに呼び出されました。DiagScenarioController.TaskWait
はそのラムダによって呼び出されたものです。
.NET イベント ビューアーを使用して nettrace を分析する
nettrace ファイルをドラッグ アンド ドロップします。
[イベント ツリー] ページに移動し、イベント "WaitHandleWaitStart" を選択し、[クエリの実行] を選択します。
WaitHandleWait イベントが生成されたスタック トレースが表示されます。 矢印をクリックして、このイベントの出力を担当するスタック トレースを展開します。
このスタックトレースは、
ManualResetEventSlim.Wait
が WaitHandleWait イベントを発生させた状態として読み取ることができます。Task.SpinThenBlockWait
によって呼び出され、それがTask.InternalWaitCore
に呼び出され、さらにTask<T>.Result
に呼び出され、最後にDiagScenario.TaskWait
に呼び出され、いくつかのラムダによって呼び出され、最終的にASP.NETコードによって呼び出されました。
実際のシナリオでは、スレッド プールの外部にあるスレッドから生成される待機イベントが多数見つかる場合があります。 スレッドプールの枯渇を調査しているため、スレッドプールの外部にある専用スレッドでの待機はすべて関連性がありません。 スタック トレースがスレッド プール スレッドからのものであるかどうかを確認する場合は、スレッド プールのメンション (たとえば、 WorkerThread.WorkerThreadStart
や ThreadPoolWorkQueue
) が含まれている必要がある最初のメソッドを確認します。
コード修正
これで、サンプル アプリの Controllers/DiagnosticScenarios.cs ファイルでこのコントローラーのコードに移動して、 await
を使用せずに非同期 API を呼び出していることを確認できます。 これは 同期オーバー非同期 コード パターンであり、スレッドをブロックすることが知られており、ThreadPool の不足の最も一般的な原因です。
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
この場合、 TaskAsyncWait()
エンドポイントに示すように、代わりに async/await を使用するようにコードを簡単に変更できます。 await を使用すると、データベース クエリの進行中に現在のスレッドが他の作業項目にサービスを提供できるようになります。 データベース検索が完了すると、ThreadPool スレッドは実行を再開します。 これにより、各要求中にコード内でスレッドがブロックされません。
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
api/diagscenario/taskasyncwait
エンドポイントに読み込みを送信するために Bombadier を実行すると、async/await アプローチを使用する場合、ThreadPool スレッド数は大幅に低いままであり、平均待機時間は 500 ミリ秒近く残ります。
>bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskasyncwait
Bombarding https://localhost:5001/api/diagscenario/taskasyncwait for 10s using 125 connection(s)
[=============================================================================================] 10s
Done!
Statistics Avg Stdev Max
Reqs/sec 227.92 274.27 1263.48
Latency 532.58ms 58.64ms 1.14s
HTTP codes:
1xx - 0, 2xx - 2390, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 98.81KB/s
.NET