Поделиться через


Отладка нехватки пула потоков

Эта статья относится к : ✔️ .NET 9.0 и более поздних версий

В этом руководстве вы узнаете, как выполнить отладку сценария голода ThreadPool. Нехватка ThreadPool возникает, когда пул не имеет доступных потоков для обработки новых рабочих элементов, и часто это приводит к медленному реагированию приложений. Используя приведенный пример веб-приложения ASP.NET Core, вы можете намеренно вызвать истощение пула потоков и узнать, как его диагностировать.

В этом руководстве описано следующее:

  • Исследовать приложение, которое медленно отвечает на запросы
  • Используйте средство dotnet-counters для выявления, вероятно, возникает голодание в ThreadPool.
  • Используйте средства dotnet-stack и dotnet-trace, чтобы определить, какая работа держит потоки ThreadPool занятыми.

Предпосылки

В руководстве используются следующие элементы:

  • Пакет SDK для .NET 9 для создания и запуска примера приложения
  • Пример веб-приложения для демонстрации проблемы истощения ресурсов в ThreadPool
  • Bombardier для создания нагрузки для примера веб-приложения
  • dotnet-counters для мониторинга счетчиков производительности
  • dotnet-stack для проверки стека потоков
  • dotnet-trace для сбора событий ожидания
  • Необязательно. PerfView для анализа событий ожидания

Запуск примера приложения

Скачайте код для примера приложения и запустите его с помощью пакета SDK для .NET:

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

Если вы используете веб-браузер и отправляете запросы https://localhost:5001/api/diagscenario/taskwait, вы увидите ответ success:taskwait , возвращенный примерно через 500 мс. Это показывает, что веб-сервер обслуживает трафик должным образом.

Наблюдение за медленной производительностью

На демонстрационном веб-сервере есть несколько конечных точек, которые имитируют запрос к базе данных и затем возвращают ответ пользователю. Каждая из этих конечных точек имеет задержку примерно в 500 мс при обслуживании запросов по одному за раз, но производительность значительно хуже, когда веб-сервер подвергается некоторой нагрузке. Скачайте средство нагрузочного тестирования 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

Эта вторая конечная точка использует шаблон кода, который выполняется еще хуже:

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 s и 15,42 соответственно). Если вы запускаете этот пример в более старой версии .NET Core, скорее всего, оба примера выполняются одинаково плохо. .NET 6 обновил эвристики ThreadPool, которые снижают влияние на производительность неправильного шаблона кодирования, используемого в первом примере.

Обнаружение голода ThreadPool

Если вы заметили поведение, приведенное выше в реальном сервисе, вы бы узнали, что он реагирует медленно под нагрузкой, но причина неизвестна. dotnet-counters — это средство, которое может отображать динамические счетчики производительности. Эти индикаторы могут предоставить подсказки о некоторых проблемах и их часто легко получить. В рабочих средах могут быть аналогичные счетчики, предоставляемые средствами удаленного мониторинга и веб-панелями мониторинга. Установите счетчики dotnet и начните мониторинг веб-службы:

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

Если приложение работает с версией .NET более ранней, чем .NET 9, выходной пользовательский интерфейс dotnet-counters будет выглядеть немного иначе; Дополнительные сведения см. в счетчиках dotnet-counters .

Приведенные выше счетчики являются примером, в то время как веб-сервер не обслуживал какие-либо запросы. Запустите Bombardier снова с конечной api/diagscenario/tasksleepwait точкой и устойчивой нагрузкой в течение 2 минут, чтобы наблюдать за тем, что происходит с счетчиками производительности.

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s

Голодание пула потоков происходит, когда нет свободных потоков для обработки рабочих элементов в очереди, и среда выполнения реагирует, увеличивая число потоков в пуле. Значение dotnet.thread_pool.thread.count быстро увеличивается до 2-3 раз больше числа ядер процессора на вашем компьютере, а затем потоки добавляются ещё по 1-2 в секунду, пока это не стабилизируется с значением более 125. Основные признаки, что нехватка потоков ThreadPool в настоящее время является ограничивающим фактором производительности, это постепенное и стабильное увеличение количества потоков ThreadPool и использование ЦП значительно ниже 100%. Увеличение количества потоков будет продолжаться до тех пор, пока пул не достигнет максимального количества, пока не будет создано достаточно потоков для обработки всех входящих рабочих элементов, или пока ЦП не будет загружен. Часто, но не всегда, недостаток потоков в 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 пул больше не голодает. Но если он стабилизируется на высоком уровне (в три раза больше или более числа ядер процессора), это обычно указывает на то, что код приложения блокирует некоторые потоки в ThreadPool, и ThreadPool компенсирует это, используя больше потоков. Устойчивое выполнение при высоких количествах потоков не обязательно будет значительно влиять на задержку запросов, но если нагрузка существенно изменяется с течением времени или приложение будет периодически перезапускаться, то каждый раз пул потоков, скорее всего, войдет в период голодания, когда он медленно увеличивает количество потоков и обеспечивает высокую задержку запросов. Каждый поток также потребляет память, поэтому сокращение общего количества потоков обеспечивает еще одно преимущество.

Начиная с .NET 6, эвристики ThreadPool были изменены, чтобы увеличить число потоков ThreadPool гораздо быстрее в ответ на некоторые блокирующие API задач. Недостаток ресурсов ThreadPool по-прежнему может происходить с этими API, но длится гораздо меньше, чем в более старых версиях .NET, потому что среда выполнения отвечает быстрее. Снова запустите Bombardier с конечной api/diagscenario/taskwait точкой:

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

В .NET 6 следует наблюдать, как пул увеличивает количество потоков быстрее, чем раньше, а затем стабилизируется на большом количестве потоков. Голодание ThreadPool происходит в то время как число потоков поднимается.

Устранение нехватки потоков в ThreadPool

Чтобы устранить нехватку ThreadPool, потоки ThreadPool должны оставаться разблокированными, чтобы быть доступными для обработки входящих рабочих элементов. Существует несколько способов определить, что делал каждый поток. Если проблема возникает только иногда, то сбор трассировки с помощью dotnet-trace лучше всего чтобы записать поведение приложения в течение определенного периода времени. Если проблема возникает постоянно, вы можете использовать средство dotnet-stack или снять дамп с помощью dotnet-dump, который можно просмотреть в Visual Studio. dotnet-stack может быть быстрее, так как он отображает стеки потоков сразу на консоли. Но отладка дампа в Visual Studio предлагает лучшую визуализацию, которая сопоставляет кадры с исходным кодом, Just My Code может отфильтровать кадры реализации среды выполнения, а функция Parallel Stacks может помочь сгруппировать большое количество потоков с аналогичными стеками. В этом руководстве показаны параметры dotnet-stack и dotnet-trace. Пример изучения стеков потоков с помощью Visual Studio см. в видеоролике по диагностике исчерпания ThreadPool.

Диагностика непрерывной проблемы с dotnet-stack

Запустите Bombardier еще раз, чтобы нагрузить веб-сервер.

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()

И кадры, расположенные в верхней части, показывают, что поток заблокирован при вызове GetResultCore(bool) функции DiagnosticScenarioController.TaskWait():

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, который можно проанализировать позже.

Существует одно конкретное событие, которое помогает диагностировать истощение пула потоков: событие WaitHandleWait, введенное в .NET 9. Он создается, когда поток блокируется такими операциями, как синхронные вызовы (например, Task.Result, и Task.WaitTask.GetAwaiter().GetResult()) или другими операциями блокировки, например lock, Monitor.Enterи ManualResetEventSlim.WaitSemaphoreSlim.Wait.

Запустите Bombardier еще раз, чтобы нагрузить веб-сервер.

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 можно проанализировать с помощью двух различных средств:

  • PerfView: средство анализа производительности, разработанное только корпорацией Майкрософт для Windows.
  • Средство просмотра событий .NET: веб-инструмент Blazor для анализа nettrace, разработанный сообществом.

В следующих разделах показано, как использовать каждое средство для чтения файла nettrace.

Анализ nettrace с помощью Perfview

  1. Скачайте PerfView и запустите его.

  2. Откройте файл nettrace, дважды щелкнув его.

    Снимок экрана открытия nettrace в PerfView

  3. Дважды щелкните Расширенная группа>Любые стеки. Откроется новое окно.

    Скриншот просмотра стеков в PerfView.

  4. Дважды щелкните строку "Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".

    Теперь вы увидите трассировки стека, в которых были созданы события WaitHandleWait. Они разделены по "WaitSource". В настоящее время существует два источника: MonitorWait для событий, создаваемых через Monitor.Wait, и Unknown для всех остальных.

    Снимок экрана представления любых стеков для событий ожидания в PerfView.

  5. Начните с MonitorWait, так как он представляет 64.8% событий. Установите флажки, чтобы рассмотреть трассировки стека, ответственные за генерацию этого события.

    Снимок экрана развернутого вида любых стеков для событий ожидания в PerfView.

    Эта трассировка стека может быть прочитана следующим образом: Task<T>.Result инициировало событие WaitHandleWait с WaitSource MonitorWait (Task<T>.Result использует Monitor.Wait для выполнения ожидания). Этот вызов был инициирован DiagScenarioController.TaskWait, который был вызван некоторой лямбдой, вызванной кодом ASP.NET.

Анализ nettrace с использованием средства просмотра событий .NET

  1. Перейдите к verdie-g.github.io/dotnet-events-viewer.

  2. Перетащите и вставьте файл nettrace.

    Скриншот открытия nettrace в обозревателе событий .NET.

  3. Перейдите на страницу "Дерево событий ", выберите событие "WaitHandleWaitStart", а затем нажмите кнопку "Выполнить запрос".

    Снимок экрана: запрос событий в средстве просмотра событий .NET.

  4. Вы должны увидеть трассировки стека, где произошли события WaitHandleWait. Нажмите на стрелки, чтобы развернуть трассировки стека, вызывающие это событие.

    Снимок экрана: представление дерева в средстве просмотра событий .NET.

    Эта трассировка стека может быть прочитана следующим образом: ManualResetEventSlim.Wait было сгенерировано событие WaitHandleWait. Task.SpinThenBlockWait, который вызван Task.InternalWaitCore, который вызван Task<T>.Result, который вызван DiagScenario.TaskWait, который вызван некоторой лямбдой, который вызван некоторым кодом ASP.NET

В реальных сценариях можно столкнуться с множеством событий ожидания, порождаемых потоками за пределами пула потоков. Здесь вы изучаете нехватку пула потоков , поэтому все ожидания выделенного потока за пределами пула потоков не актуальны. Вы можете определить, является ли трассировка стека из потока пула, просматривая первичные методы, которые должны содержать упоминание пула потоков (например, WorkerThread.WorkerThreadStart или ThreadPoolWorkQueue).

Верхняя часть трассировки стека потока в пуле потоков.

Исправление кода

Теперь вы можете перейти к коду этого контроллера в файле Controllers/DiagnosticScenarios.cs в примере приложения, чтобы увидеть, что он вызывает асинхронный API без использования await. Это шаблон кода синхронизации над асинхронным кодом, который, как известно, блокирует потоки и является наиболее распространенной причиной нехватки ThreadPool.

public ActionResult<string> TaskWait()
{
    // ...
    Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
    return "success:taskwait";
}

В этом случае код можно легко изменить для использования конструкций async и await вместо этого, как показано в конечной точке TaskAsyncWait(). Использование await позволяет текущему потоку обслуживать другие рабочие объекты во время выполнения запроса базы данных. После завершения поиска базы данных поток ThreadPool возобновляет выполнение. Таким образом поток не блокируется в коде во время каждого запроса.

public async Task<ActionResult<string>> TaskAsyncWait()
{
    // ...
    Customer c = await PretendQueryCustomerFromDbAsync("Dana");
    return "success:taskasyncwait";
}

Запуск Bombadier для отправки нагрузки в конечную точку api/diagscenario/taskasyncwait показывает, что количество потоков в ThreadPool остается значительно ниже, а средняя задержка остается около 500 мс при использовании подхода async/await.

>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