Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Este artigo se aplica a: ✔️ .NET 9.0 e versões posteriores
Neste tutorial, você aprenderá a depurar um cenário de esgotamento do ThreadPool. A escassez de ThreadPool ocorre quando o pool não tem threads disponíveis para desempenhar novos itens de trabalho e geralmente faz com que os aplicativos respondam lentamente. Usando o exemplo fornecido aplicativo web ASP.NET Core, você pode causar intencionalmente a exaustão do pool de threads e aprender como diagnosticá-la.
Neste tutorial, você irá:
- Investigar um aplicativo que está respondendo às solicitações lentamente
- Use a ferramenta dotnet-counters para identificar se provavelmente está ocorrendo saturação do ThreadPool.
- Use as ferramentas dotnet-stack e dotnet-trace para determinar qual trabalho está mantendo ocupados os threads do ThreadPool.
Pré-requisitos
O tutorial usa:
- SDK do .NET 9 para compilar 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
- Para examinar pilhas de thread, use dotnet-stack
- dotnet-trace para coletar eventos de espera
- Opcional: PerfView para analisar os eventos de espera
Executar o aplicativo de exemplo
Baixe o código do 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ê utilizar um navegador da web e enviar solicitações para https://localhost:5001/api/diagscenario/taskwait
, deverá ver a resposta success:taskwait
retornada após cerca de 500 ms. Isso mostra que o servidor Web está atendendo ao tráfego conforme o esperado.
Observe o desempenho lento
O servidor Web de demonstração tem vários pontos de extremidade que simulam a realização de uma solicitação de banco de dados e, em seguida, retornam uma resposta ao usuário. 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 é submetido a alguma carga. Baixe a ferramenta de teste de carga Bombardier e observe a diferença de 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 endpoint 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 pontos de extremidade mostram significativamente 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, é provável que ambos os exemplos sejam executados igualmente mal. O .NET 6 atualizou a heurística do ThreadPool que reduz o impacto no desempenho do padrão de codificação incorreto usado no primeiro exemplo.
Detectar o esgotamento 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. Esses contadores podem fornecer pistas sobre certos problemas e muitas vezes são 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 contadores dotnet e comece a monitorar 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
Os contadores anteriores são um exemplo enquanto o servidor Web não estava atendendo solicitações. Execute o Bombardier novamente com o endpoint api/diagscenario/tasksleepwait
e carga sustentada por 2 minutos, permitindo tempo suficiente para observar o que acontece com os contadores de desempenho.
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 o runtime reage aumentando o número de threads do ThreadPool. O valor dotnet.thread_pool.thread.count
aumenta rapidamente para 2-3 vezes o número de núcleos de processador no seu computador e, em seguida, threads adicionais são adicionados à taxa de 1 a 2 por segundo até estabilizar acima de 125. Os principais sinais de que a fome do ThreadPool é atualmente um gargalo de desempenho são o aumento lento e constante dos threads do ThreadPool e do uso da CPU muito menos de 100%. O aumento da contagem de threads continuará até que o pool atinja o número máximo de threads, sejam criadas threads suficientes para satisfazer todos os itens de trabalho de entrada, ou a CPU seja saturada. Muitas vezes, mas nem sempre, a saturação do ThreadPool também mostrará valores grandes para dotnet.thread_pool.queue.length
e valores baixos para dotnet.thread_pool.work_item.count
, o que significa que há uma grande quantidade de trabalho pendente e pouco trabalho sendo 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
Depois que a contagem de threads do ThreadPool se estabilizar, o pool não estará mais sobrecarregado. Mas, se ele se estabilizar em um valor alto (mais de três vezes o número de núcleos do processador), isso geralmente indica que o código do aplicativo está bloqueando alguns threads do ThreadPool e que o ThreadPool está compensando executando com mais threads. Operar de forma constante com altos números de threads não necessariamente terá 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 esgotamento, durante o qual aumentará lentamente o número de threads e resultará em alta latência nas solicitações. Cada thread também consome memória, portanto, reduzir o número total de threads necessários oferece outro benefício.
A partir do .NET 6, a heurística do ThreadPool foi modificada para aumentar o número de threads do ThreadPool muito mais rapidamente em resposta a determinadas APIs de tarefa de bloqueio. O esgotamento do ThreadPool ainda pode ocorrer com essas APIs, mas a duração é muito mais breve do que era com versões mais antigas do .NET, porque o ambiente de execução responde mais rapidamente. Execute o Bombardier novamente com o api/diagscenario/taskwait
endpoint:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
No .NET 6, você deve observar que o pool aumenta a contagem de threads mais rapidamente do que antes e, em seguida, estabiliza em um alto número de threads. A fome do ThreadPool está ocorrendo enquanto a contagem de threads está subindo.
Resolver a insuficiência de recursos do ThreadPool
Para eliminar a exaustão do ThreadPool, as threads do ThreadPool precisam permanecer desbloqueadas para que estejam disponíveis para lidar com tarefas de trabalho pendentes. Há várias maneiras de determinar o que cada thread estava fazendo. Se o problema ocorrer apenas ocasionalmente, coletar um rastreamento com o dotnet-trace é a melhor maneira de registrar o comportamento do aplicativo ao longo de um período de tempo. Se o problema ocorrer constantemente, você poderá usar a ferramenta dotnet-stack ou capturar um despejo 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 análise de dump do Visual Studio oferece melhores visualizações que mapeiam quadros para a origem, o Just My Code pode filtrar quadros de implementação de runtime, e o recurso Pilhas Paralelas pode ajudar a agrupar um grande volume de threads com pilhas semelhantes. Este tutorial mostra as opções dotnet-stack e dotnet-trace. Para obter um exemplo de investigação das pilhas de threads usando o Visual Studio, consulte o vídeo tutorial de Diagnosticando o esgotamento do ThreadPool.
Diagnosticar um problema contínuo com a stack do .NET
Execute o 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
Você deverá ver uma saída longa contendo um grande número de pilhas, muitas das quais têm esta aparência:
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 do ThreadPool:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
E os quadros próximos à parte superior revelam que o thread está bloqueado em uma chamada da função DiagnosticScenarioController.TaskWait() para 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()
Diagnosticar um problema intermitente usando o dotnet-trace
A abordagem dotnet-stack é eficaz apenas para operações de bloqueio óbvias e consistentes que ocorrem em cada solicitação. Em alguns cenários, o bloqueio ocorre esporadicamente apenas a cada alguns minutos, tornando o dotnet-stack menos útil para diagnosticar o problema. Nesse caso, você pode usar o 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 diagnosticar o esgotamento do pool de threads: o evento WaitHandleWait, que foi introduzido no .NET 9. É emitido quando uma thread é bloqueada 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 o 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 nomeado DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace
contendo os eventos. Esse nettrace pode ser analisado usando duas ferramentas diferentes:
- PerfView: uma ferramenta de análise de desempenho desenvolvida apenas pela Microsoft para Windows.
- Visualizador de Eventos do .NET: uma ferramenta Web Blazor de análise de nettrace desenvolvida pela comunidade.
As seções a seguir mostram como usar cada ferramenta para ler o arquivo nettrace.
Analisar um nettrace com o PerfView
Baixe o PerfView e execute-o.
Abra o arquivo nettrace clicando duas vezes nele.
Clique duas vezes em Grupo Avançado>Qualquer Pilhas. Uma nova janela é aberta.
Clique duas vezes na linha "Evento Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".
Agora você deve ver os rastreamentos de pilha em que os eventos WaitHandleWait foram emitidos. Eles são separados por "WaitSource". Atualmente, há duas fontes:
MonitorWait
para eventos emitidos por meio de Monitor.Wait eUnknown
para todas as outras.Comece com MonitorWait, pois ele representa 64,8% dos eventos. Você pode marcar as caixas de seleção para expandir os rastreamentos de pilha responsáveis pela emissão desse evento.
Este rastreamento de pilha pode ser lido como:
Task<T>.Result
emitiu um evento WaitHandleWait com WaitSource MonitorWait (Task<T>.Result
usaMonitor.Wait
para realizar uma espera). Foi chamado porDiagScenarioController.TaskWait
, que foi chamado por algum lambda, que foi chamado por algum código ASP.NET
Analisar um nettrace com o Visualizador de Eventos do .NET
Arraste e solte o arquivo nettrace.
Vá para a página Árvore de Eventos , selecione o evento "WaitHandleWaitStart" e selecione Executar consulta.
Você deve ver os rastreamentos de pilha em que os eventos WaitHandleWait foram emitidos. Clique nas setas para expandir os rastros de pilha responsáveis por emitir esse evento.
Esse rastreamento de pilha pode ser lido como:
ManualResetEventSlim.Wait
emitiu 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 gerados por threads fora do grupo de threads. Aqui, você está investigando uma exaustão de pool de threads, portanto, todas as esperas em thread dedicado fora do pool de threads não são relevantes. Você pode identificar se um rastreamento de pilha é proveniente de um pool de threads examinando os primeiros métodos, que devem conter uma menção ao pool de threads (por exemplo, WorkerThread.WorkerThreadStart
ou ThreadPoolWorkQueue
).
Correção de código
Agora você pode navegar até o código desse controlador no arquivo Controllers/DiagnosticScenarios.cs do aplicativo de exemplo para ver que ele está chamando uma API assíncrona sem usar await
. Esse é o padrão de sync-over-async, que é conhecido por bloquear as threads e é a causa mais comum de esgotamento do ThreadPool.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
Nesse caso, o código pode ser facilmente alterado para usar o async/await, conforme mostrado no TaskAsyncWait()
endpoint. O uso de await permite que o thread atual processe outros itens de trabalho enquanto a consulta ao banco de dados está em andamento. Quando a pesquisa de banco de dados for concluída, um thread do ThreadPool retomará a execução. Dessa 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";
}
Executar Bombadier para enviar carga ao endpoint api/diagscenario/taskasyncwait
mostra que a contagem de threads do ThreadPool permanece muito menor e que a latência média fica próxima de 500ms ao usar a abordagem de 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