Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Эта статья относится к : ✔️ .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
Скачайте PerfView и запустите его.
Откройте файл nettrace, дважды щелкнув его.
Дважды щелкните Расширенная группа>Любые стеки. Откроется новое окно.
Дважды щелкните строку "Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".
Теперь вы увидите трассировки стека, в которых были созданы события WaitHandleWait. Они разделены по "WaitSource". В настоящее время существует два источника:
MonitorWaitдля событий, создаваемых через Monitor.Wait, иUnknownдля всех остальных.Начните с MonitorWait, так как он представляет 64.8% событий. Установите флажки, чтобы рассмотреть трассировки стека, ответственные за генерацию этого события.
Эта трассировка стека может быть прочитана следующим образом:
Task<T>.Resultинициировало событие WaitHandleWait с WaitSource MonitorWait (Task<T>.ResultиспользуетMonitor.Waitдля выполнения ожидания). Этот вызов был инициированDiagScenarioController.TaskWait, который был вызван некоторой лямбдой, вызванной кодом ASP.NET.
Анализ nettrace с использованием средства просмотра событий .NET
Перейдите к verdie-g.github.io/dotnet-events-viewer.
Перетащите и вставьте файл nettrace.
Перейдите на страницу "Дерево событий ", выберите событие "WaitHandleWaitStart", а затем нажмите кнопку "Выполнить запрос".
Вы должны увидеть трассировки стека, где произошли события WaitHandleWait. Нажмите на стрелки, чтобы развернуть трассировки стека, вызывающие это событие.
Эта трассировка стека может быть прочитана следующим образом:
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