次の方法で共有


ThreadPool の枯渇をデバッグする

この記事の対象: ✔️ .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.ResultTask.WaitTask.GetAwaiter().GetResult()など)や他のロック操作(lockMonitor.EnterManualResetEventSlim.WaitSemaphoreSlim.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 つの異なるツールを使用して分析できます。

次のセクションでは、各ツールを使用して nettrace ファイルを読み取る方法を示します。

Perfview を使用して nettrace を分析する

  1. PerfView をダウンロードして実行します。

  2. nettrace ファイルをダブルクリックして開きます。

    PerfView で nettrace を開くスクリーンショット

  3. 高度なグループ>任意のスタックをダブルクリックします。 新しいウィンドウが開きます。

    PerfView のスタック ビューのスクリーンショット。

  4. "Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start" 行をダブルクリックします。

    これで、WaitHandleWait イベントが生成されたスタック トレースが表示されます。 "WaitSource" で分割されます。 現在、2 つのソースがあります。MonitorWait を介して生成されたイベントのと、他のすべてのイベントのUnknownです。

    PerfView の待機イベントのスタック ビューのスクリーンショット。

  5. 64.8% のイベントを表す MonitorWait から始めます。 チェック ボックスをオンにして、このイベントの出力を担当するスタック トレースを展開できます。

    PerfView の待機イベントの展開された任意のスタック ビューのスクリーンショット。

    このスタックトレースは次のように読み取ることができます:Task<T>.Result が WaitHandleWait イベントを送信しました、そしてこのイベントは WaitSource MonitorWait を使用しています (Task<T>.Result は待機を行うために Monitor.Wait を使用しています)。 これはあるラムダによって呼び出され、そのラムダはあるASP.NETコードに呼び出されました。DiagScenarioController.TaskWait はそのラムダによって呼び出されたものです。

.NET イベント ビューアーを使用して nettrace を分析する

  1. verdie-g.github.io/dotnet-events-viewer に移動します。

  2. nettrace ファイルをドラッグ アンド ドロップします。

    .NET イベント ビューアーで nettrace を開くスクリーンショット。

  3. [イベント ツリー] ページに移動し、イベント "WaitHandleWaitStart" を選択し、[クエリの実行] を選択します。

    .NET イベント ビューアーのイベント クエリのスクリーンショット。

  4. WaitHandleWait イベントが生成されたスタック トレースが表示されます。 矢印をクリックして、このイベントの出力を担当するスタック トレースを展開します。

    .NET イベント ビューアーのツリー ビューのスクリーンショット。

    このスタックトレースは、ManualResetEventSlim.Wait が WaitHandleWait イベントを発生させた状態として読み取ることができます。 Task.SpinThenBlockWaitによって呼び出され、それがTask.InternalWaitCoreに呼び出され、さらにTask<T>.Resultに呼び出され、最後にDiagScenario.TaskWaitに呼び出され、いくつかのラムダによって呼び出され、最終的にASP.NETコードによって呼び出されました。

実際のシナリオでは、スレッド プールの外部にあるスレッドから生成される待機イベントが多数見つかる場合があります。 スレッドプールの枯渇を調査しているため、スレッドプールの外部にある専用スレッドでの待機はすべて関連性がありません。 スタック トレースがスレッド プール スレッドからのものであるかどうかを確認する場合は、スレッド プールのメンション (たとえば、 WorkerThread.WorkerThreadStartThreadPoolWorkQueue) が含まれている必要がある最初のメソッドを確認します。

スレッドプールのスレッドスタックトレースのトップ。

コード修正

これで、サンプル アプリの 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