Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Este artigo aplica-se a: ✔️ .NET 9.0 e versões posteriores
Neste tutorial, você aprenderá como depurar um cenário de fome do ThreadPool. A inanição do ThreadPool ocorre quando o pool não tem threads disponíveis para processar novos itens de trabalho e geralmente faz com que os aplicativos respondam lentamente. Usando o exemplo fornecido aplicativo web ASP.NET Core, pode provocar intencionalmente a saturação do ThreadPool e aprender como diagnosticá-la.
Neste tutorial, você irá:
- Investigar um aplicativo que está respondendo a solicitações lentamente
- Use a ferramenta dotnet-counters para identificar a probabilidade de ocorrer fome no ThreadPool
- Use as ferramentas dotnet-stack e dotnet-trace para determinar qual trabalho está mantendo os threads do ThreadPool ocupados
Pré-requisitos
O tutorial usa:
- SDK do .NET 9 para criar e executar o aplicativo de exemplo
- Aplicativo Web de exemplo para demonstrar o comportamento de fome do ThreadPool
- Bombardier para gerar carga para o aplicativo Web de exemplo
- dotnet-counters para observar contadores de desempenho
- dotnet-stack para examinar pilhas de threads
- dotnet-trace para coletar eventos de espera
- Opcional: PerfView para analisar os eventos de espera
Executar o aplicativo de exemplo
Baixe o código para o aplicativo de exemplo e execute-o usando o SDK do .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
Se você usa um navegador da Web e envia solicitações para https://localhost:5001/api/diagscenario/taskwaito , você verá a resposta success:taskwait retornada após cerca de 500 ms. Isso mostra que o servidor Web está servindo o tráfego conforme o esperado.
Observe o desempenho lento
O servidor web de demonstração tem vários endpoints que simulam uma solicitação ao banco de dados e depois retornam uma resposta ao utilizador. Cada um desses pontos de extremidade tem um atraso de aproximadamente 500 ms ao atender solicitações uma de cada vez, mas o desempenho é muito pior quando o servidor Web está sujeito a alguma carga. Faça o download da ferramenta de teste de carga da Bombardier e observe a diferença na latência quando 125 solicitações simultâneas são enviadas para cada ponto de extremidade.
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
Este segundo ponto de extremidade usa um padrão de código que tem um desempenho ainda pior:
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
Ambos os endpoints mostram dramaticamente mais do que a latência média de 500 ms quando a carga é alta (3,48 s e 15,42 s, respectivamente). Se você executar este exemplo em uma versão mais antiga do .NET Core, provavelmente verá ambos os exemplos terem um desempenho igualmente ruim. O .NET 6 atualizou a heurística do ThreadPool que reduz o impacto no desempenho do padrão de codificação incorreta usado no primeiro exemplo.
Detetar saturação do ThreadPool
Se você observasse o comportamento acima em um serviço do mundo real, saberia que ele está respondendo lentamente sob carga, mas não saberia a causa. dotnet-counters é uma ferramenta que pode mostrar contadores de desempenho ao vivo. Estes contadores podem fornecer pistas sobre certos problemas e são muitas vezes fáceis de obter. Em ambientes de produção, você pode ter contadores semelhantes fornecidos por ferramentas de monitoramento remoto e painéis da Web. Instale o dotnet-counters e comece a monitorizar o serviço 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
Se seu aplicativo estiver executando uma versão do .NET mais antiga que o .NET 9, a interface do usuário de saída dos contadores de pontos terá uma aparência ligeiramente diferente; Consulte contadores de pontos para obter detalhes.
Os contadores anteriores são um exemplo quando o servidor web não estava a processar nenhum pedido. Execute novamente o Bombardier com o endpoint api/diagscenario/tasksleepwait e uma carga sustentada de 2 minutos para ter tempo suficiente para observar o que acontece com os indicadores de performance.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
O esgotamento do ThreadPool ocorre quando não há threads livres para lidar com os itens de trabalho enfileirados e a execução responde aumentando o número de threads do ThreadPool. O dotnet.thread_pool.thread.count valor aumenta rapidamente para 2-3x o número de núcleos de processador em sua máquina e, em seguida, mais threads são adicionados 1-2 por segundo até estabilizar em algum lugar acima de 125. Os principais sinais de que a fome do ThreadPool é atualmente um gargalo de desempenho são o aumento lento e constante de threads do ThreadPool e o uso da CPU muito inferior a 100%. O aumento da contagem de threads continuará até que o pool atinja o número máximo de threads, tenham sido criadas threads suficientes para satisfazer todos os itens de trabalho recebidos ou a CPU esteja saturada. Muitas vezes, mas nem sempre, a saturação do ThreadPool também mostrará valores grandes para dotnet.thread_pool.queue.length e baixos para dotnet.thread_pool.work_item.count, o que significa que há uma grande quantidade de trabalho pendente e pouco trabalho concluído. Aqui está um exemplo dos contadores enquanto a contagem de threads ainda está aumentando:
[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
Uma vez que a contagem de threads do ThreadPool estabiliza, o pool deixa de estar em situação de escassez. Mas se estabilizar num valor alto (mais de cerca de três vezes o número de núcleos do processador), isso geralmente indica que o código da aplicação está a bloquear alguns threads do ThreadPool e o ThreadPool compensa executando com mais threads. A execução constante em contagens altas de threads não terá necessariamente grandes impactos na latência da solicitação, mas se a carga variar drasticamente ao longo do tempo ou se o aplicativo for reiniciado periodicamente, cada vez que o ThreadPool provavelmente entrará em um período de fome em que está aumentando lentamente os threads e entregando baixa latência de solicitação. Cada thread também consome memória, portanto, reduzir o número total de threads necessários fornece outro benefício.
A partir do .NET 6, as heurísticas do ThreadPool foram modificadas para aumentar o número de threads do ThreadPool muito mais rapidamente em resposta a determinadas APIs de tarefas de bloqueio. A escassez do ThreadPool ainda pode ocorrer com estas APIs, mas a duração é muito mais breve do que nas versões mais antigas do .NET, pois o tempo de execução responde mais rapidamente. Execute Bombardier novamente com o endpoint api/diagscenario/taskwait:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
No .NET 6, você deve observar o pool aumentar a contagem de threads mais rapidamente do que antes e, em seguida, estabilizar em um número alto de threads. A saturação do ThreadPool está ocorrendo enquanto a contagem de threads está a aumentar.
Resolver a escassez do ThreadPool
Para eliminar a saturação do ThreadPool, os threads do ThreadPool precisam permanecer desbloqueados para que estejam disponíveis para lidar com itens de trabalho recebidos. Há várias maneiras de determinar o que cada thread estava fazendo. Se o problema ocorrer apenas ocasionalmente, a coleta de um rastreamento com dotnet-trace é melhor para registrar o comportamento do aplicativo durante um período de tempo. Se o problema ocorrer constantemente, você pode usar a ferramenta dotnet-stack ou capturar um dump com dotnet-dump que pode ser exibido no Visual Studio. dotnet-stack pode ser mais rápido porque mostra as pilhas de threads imediatamente no console. Mas a depuração de "dump" do Visual Studio oferece melhor visualização ao mapear molduras para o código-fonte, o recurso "Just My Code" pode filtrar molduras de implementação em tempo de execução, e o recurso "Pilhas Paralelas" pode ajudar a agrupar um grande número de threads com pilhas semelhantes. Este tutorial mostra as opções dotnet-stack e dotnet-trace. Para um exemplo de análise das pilhas de threads usando o Visual Studio, consulte o vídeo Tutorial Diagnosing ThreadPool Starvation.
Diagnosticar um problema contínuo com dotnet-stack
Execute a Bombardier novamente para colocar o servidor web sob carga:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Em seguida, execute dotnet-stack para ver os rastreamentos da pilha de threads:
dotnet-stack report -n DiagnosticScenarios
Deverá ver um output extenso contendo um grande número de pilhas, muitas das quais se assemelham a isto:
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()
Os quadros na parte inferior dessas pilhas indicam que esses threads são threads ThreadPool:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
E os frames perto do topo revelam que o thread está bloqueado numa chamada para GetResultCore(bool) na função 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()
Para diagnosticar um problema intermitente com o dotnet-trace
A abordagem dotnet-stack é eficaz apenas para operações de bloqueio óbvias e consistentes que ocorrem em todas as solicitações. Em alguns cenários, o bloqueio acontece esporadicamente apenas a cada poucos minutos, tornando o dotnet-stack menos útil para diagnosticar o problema. Nesse caso, você pode usar dotnet-trace para coletar eventos durante um período de tempo e salvá-los em um arquivo nettrace que pode ser analisado posteriormente.
Há um evento específico que ajuda a identificar a saturação do pool de threads: o evento WaitHandleWait, que foi introduzido no .NET 9. É emitido quando um thread é bloqueado por operações como chamadas sync-over-async (por exemplo, Task.Result, Task.Wait, e Task.GetAwaiter().GetResult()) ou por outras operações de bloqueio como lock, Monitor.Enter, ManualResetEventSlim.Wait, e SemaphoreSlim.Wait.
Execute a Bombardier novamente para colocar o servidor web sob carga:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Em seguida, execute dotnet-trace para coletar eventos de espera:
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
Isso deve gerar um arquivo chamado DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace contendo os eventos. Este rastreamento de rede pode ser analisado usando duas ferramentas distintas:
- PerfView: Uma ferramenta de análise de desempenho desenvolvida pela Microsoft apenas para Windows.
- .NET Events Viewer: Uma ferramenta web Blazor de análise nettrace desenvolvida pela comunidade.
As seções a seguir mostram como usar cada ferramenta para ler o arquivo nettrace.
Analise um nettrace com PerfView
Baixe PerfView e execute-o.
Abra o arquivo nettrace clicando duas vezes nele.
Clique duas vezes em Advanced Group>Any Stacks. Abre-se uma nova janela.
Clique duas vezes na linha "Evento Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".
Agora deverá ver os rastros de pilha onde os eventos WaitHandleWait foram emitidos. Eles são divididos por "WaitSource". Atualmente existem duas fontes:
MonitorWaitpara eventos emitidos através do Monitor.Wait, eUnknownpara todos os outros.Comece com MonitorWait, pois ele representa 64,8% dos eventos. Você pode marcar os checkboxes para a expansão dos rastreamentos de pilha responsáveis pela emissão deste evento.
Esse rastreamento de pilha pode ser lido como:
Task<T>.Resultemitiu um evento WaitHandleWait com um WaitSource MonitorWait (Task<T>.ResultusaMonitor.Waitpara executar uma espera). Foi chamado porDiagScenarioController.TaskWait, que foi chamado por um lambda, que foi chamado por algum código ASP.NET
Analise um nettrace com o .NET Visualizador de Eventos
Arraste e solte o arquivo nettrace.
Vá para a página Árvore de eventos , selecione o evento "WaitHandleWaitStart" e, em seguida, selecione Executar consulta.
Você deve ver os traços da pilha onde os eventos WaitHandleWait foram gerados. Clique nas setas para expandir os rastreamentos de pilha responsáveis pela geração deste evento.
Este rasto de pilha pode ser lido como:
ManualResetEventSlim.Waitemitiu um evento WaitHandleWait. Foi chamado porTask.SpinThenBlockWait, que foi chamado porTask.InternalWaitCore, que foi chamado porTask<T>.Result, que foi chamado porDiagScenario.TaskWait, que foi chamado por algum lambda, que foi chamado por algum código ASP.NET
Em cenários do mundo real, você pode encontrar muitos eventos de espera emitidos por threads fora do pool de threads. Aqui, está a investigar o esgotamento de um pool de threads, portanto, todas as esperas em threads dedicadas fora do pool de threads não são relevantes. É possível determinar se um rastreamento de pilha é de um thread de pool ao observar os métodos iniciais, que devem conter uma menção ao pool de threads (por exemplo, WorkerThread.WorkerThreadStart ou ThreadPoolWorkQueue).
Correção de código
Agora pode navegar até o código deste controlador no arquivo Controllers/DiagnosticScenarios.cs do aplicativo de exemplo para ver que está a chamar uma API assíncrona sem usar await. Este é o padrão de código sync-over-async, que é conhecido por ser bloqueador de threads e é a causa mais comum de exaustão do ThreadPool.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
Neste caso, o código pode ser prontamente alterado para usar async/await, conforme mostrado no TaskAsyncWait() endpoint. O uso de await permite que o thread atual atenda outros itens de trabalho enquanto a consulta ao banco de dados está em andamento. Quando a pesquisa do banco de dados estiver concluída, um thread ThreadPool retomará a execução. Desta forma, nenhum thread é bloqueado no código durante cada solicitação.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
A execução do Bombadier para enviar carga para o api/diagscenario/taskasyncwait endpoint mostra que a contagem de threads do ThreadPool permanece muito menor e a latência média permanece perto de 500ms ao usar a abordagem 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