Partilhar via


Depurar saturação do ThreadPool

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:

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

  1. Baixe PerfView e execute-o.

  2. Abra o arquivo nettrace clicando duas vezes nele.

    Captura de ecrã da abertura de um nettrace no PerfView

  3. Clique duas vezes em Advanced Group>Any Stacks. Abre-se uma nova janela.

    Captura de ecrã da visualização de pilhas no PerfView.

  4. 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: MonitorWait para eventos emitidos através do Monitor.Wait, e Unknown para todos os outros.

    Captura de tela da exibição de quaisquer pilhas para eventos de espera no PerfView.

  5. 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.

    Captura de ecrã da vista expandida de quaisquer pilhas para eventos de espera no PerfView.

    Esse rastreamento de pilha pode ser lido como: Task<T>.Result emitiu um evento WaitHandleWait com um WaitSource MonitorWait (Task<T>.Result usa Monitor.Wait para executar uma espera). Foi chamado por DiagScenarioController.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

  1. Vá para verdie-g.github.io/dotnet-events-viewer.

  2. Arraste e solte o arquivo nettrace.

    Captura de ecrã da abertura de um nettrace no Visualizador de Eventos .NET.

  3. Vá para a página Árvore de eventos , selecione o evento "WaitHandleWaitStart" e, em seguida, selecione Executar consulta.

    Captura de ecrã de uma consulta de eventos no .NET Events Viewer.

  4. 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.

    Captura de ecrã da vista em árvore no Visualizador de Eventos .NET.

    Este rasto de pilha pode ser lido como: ManualResetEventSlim.Wait emitiu um evento WaitHandleWait. Foi chamado por Task.SpinThenBlockWait, que foi chamado por Task.InternalWaitCore, que foi chamado por Task<T>.Result, que foi chamado por DiagScenario.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).

Parte superior do rastro de pilha de um thread de pool de threads.

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