Condividi tramite


Debug dell'esaurimento del ThreadPool

Questo articolo si applica a: ✔️ .NET 9.0 e versioni successive

In questa esercitazione imparerai come eseguire il debug di una situazione di carenza di risorse del ThreadPool. ThreadPool starvation si verifica quando il pool non dispone di thread disponibili per elaborare nuovi elementi di lavoro e spesso fa sì che le applicazioni rispondano lentamente. Usando l'esempio fornito app Web ASP.NET Core, è possibile causare l'esaurimento del ThreadPool intenzionalmente e imparare a diagnosticare il problema.

In questa esercitazione si eseguiranno le seguenti attività:

  • Analizzare un'app che risponde alle richieste lentamente
  • Usare lo strumento dotnet-counters per identificare che è probabile che si stia verificando un problema di esaurimento del ThreadPool.
  • Usare gli strumenti dotnet-stack e dotnet-trace per determinare il lavoro che mantiene occupato i thread ThreadPool

Prerequisiti

Il tutorial utilizza:

Eseguire l'app di esempio

Scaricare il codice per l'app di esempio ed eseguirlo usando .NET SDK:

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 si usa un Web browser e si inviano richieste a https://localhost:5001/api/diagscenario/taskwait, verrà visualizzata la risposta success:taskwait restituita dopo circa 500 ms. Ciò mostra che il server Web gestisce il traffico come previsto.

Notare prestazioni lente

Il server Web demo include diversi endpoint che simulano l'invio di una richiesta di database e quindi la restituzione di una risposta all'utente. Ognuno di questi endpoint ha un ritardo di circa 500 ms durante la gestione delle richieste una alla volta, ma le prestazioni sono molto peggiori quando il server Web è soggetto a un certo carico. Scaricare lo strumento di test di carico bombardiere e osservare la differenza di latenza quando vengono inviate 125 richieste simultanee a ogni endpoint.

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

Questo secondo endpoint usa un modello di codice che esegue ancora peggio:

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

Entrambi questi endpoint mostrano significativamente più della latenza media di 500 ms quando il carico è elevato (rispettivamente 3,48 s e 15,42 s). Se si esegue questo esempio in una versione precedente di .NET Core, è probabile che entrambi gli esempi vengano eseguiti altrettanto male. .NET 6 ha aggiornato l'euristica threadPool che riduce l'impatto sulle prestazioni del modello di codifica non valido usato nel primo esempio.

Rilevare l'esaurimento del ThreadPool

Se si osserva il comportamento precedente in un servizio reale, si sa che risponde lentamente sotto carico, ma non si conosce la causa. dotnet-counters è uno strumento che può visualizzare i contatori delle prestazioni live. Questi contatori possono fornire indizi su determinati problemi e spesso sono facili da ottenere. Negli ambienti di produzione potrebbero essere presenti contatori simili forniti da strumenti di monitoraggio remoto e dashboard Web. Installare dotnet-counters e iniziare a monitorare il servizio 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 l'app esegue una versione di .NET precedente a .NET 9, l'interfaccia utente di output dei contatori dotnet sarà leggermente diversa; per informazioni dettagliate, vedere dotnet-counters .

I contatori precedenti sono un esempio mentre il server Web non stava eseguendo alcuna richiesta. Eseguire nuovamente Bombardier con l'endpoint api/diagscenario/tasksleepwait e il carico sostenuto per 2 minuti, in modo da avere abbastanza tempo per osservare cosa accade ai contatori delle prestazioni.

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

La saturazione del ThreadPool si verifica quando non sono presenti thread liberi per gestire i task in coda e il runtime risponde aumentando il numero di thread del ThreadPool. Il dotnet.thread_pool.thread.count valore aumenta rapidamente fino a raggiungere 2-3 volte il numero di core del processore sulla tua macchina, e quindi vengono aggiunti ulteriori thread al ritmo di 1-2 al secondo fino a quando non si stabilizza da qualche parte sopra 125. I segnali principali che la fame di ThreadPool è attualmente un collo di bottiglia delle prestazioni sono l'aumento lento e costante dei thread ThreadPool e dell'utilizzo della CPU molto inferiore a 100%. L'aumento del numero di thread continuerà fino a quando il pool raggiunge il numero massimo di thread, sono stati creati thread sufficienti per soddisfare tutti gli elementi di lavoro in ingresso o la CPU è stata satura. Spesso, ma non sempre, l'affamamento del ThreadPool mostrerà anche valori elevati per dotnet.thread_pool.queue.length e valori bassi per dotnet.thread_pool.work_item.count, ovvero c'è una grande quantità di lavoro in sospeso e poco lavoro completato. Ecco un esempio dei contatori mentre il conteggio dei thread è ancora in aumento:

[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

Una volta stabilizzato il numero di thread ThreadPool, il pool non è più affamato. Tuttavia, se si stabilizza su un valore elevato (più di circa tre volte il numero di core del processore), di solito indica che il codice dell'applicazione blocca alcuni thread del ThreadPool e il ThreadPool compensa eseguendo più thread. L'esecuzione costante a un numero elevato di thread non avrà necessariamente un impatto significativo sulla latenza delle richieste, ma se il carico varia notevolmente nel tempo o l'app verrà riavviata periodicamente, ogni volta il ThreadPool potrebbe entrare in un periodo di saturazione in cui incrementa lentamente il numero di thread e causa una latenza scarsa nelle richieste. Ogni thread utilizza anche memoria, quindi la riduzione del numero totale di thread necessari offre un altro vantaggio.

A partire da .NET 6, l'euristica threadPool è stata modificata per aumentare il numero di thread ThreadPool molto più velocemente in risposta a determinate API di attività di blocco. ThreadPool starvation può comunque verificarsi con queste API, ma la durata è molto più breve rispetto a quella delle versioni precedenti di .NET perché il runtime risponde più rapidamente. Eseguire di nuovo Bombardier con l'endpoint api/diagscenario/taskwait :

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

In .NET 6 è necessario osservare che il pool aumenta il numero di thread più rapidamente di prima e quindi stabilizza in un numero elevato di thread. Si sta verificando un esaurimento del ThreadPool mentre il conteggio dei thread sta aumentando.

Risolvere l'insufficienza del ThreadPool

Per eliminare l'esaurimento del ThreadPool, i thread del ThreadPool devono rimanere sbloccati in modo che siano disponibili per poter gestire gli elementi di lavoro in arrivo. Esistono diversi modi per determinare le operazioni eseguite da ogni thread. Se il problema si verifica solo occasionalmente, è preferibile raccogliere una traccia con dotnet-trace per registrare il comportamento dell'applicazione nel corso del tempo. Se il problema si verifica costantemente, è possibile usare lo strumento dotnet-stack o acquisire un dump con dotnet-dump che può essere visualizzato in Visual Studio. dotnet-stack può essere più veloce perché mostra immediatamente gli stack di thread nella console. Tuttavia, il debug del dump di Visual Studio offre visualizzazioni migliori che mappano i frame all'origine; Just My Code può filtrare i frame dell'implementazione runtime e la caratteristica Stack paralleli può aiutare a raggruppare un numero elevato di thread con stack simili. Questa esercitazione illustra le opzioni dotnet-stack e dotnet-trace. Per un esempio di analisi degli stack di thread con Visual Studio, vedere il video dell'esercitazione sulla diagnosi della fame di ThreadPool.

Diagnosticare un problema continuo con dotnet-stack

Eseguire di nuovo Bombardier per caricare il server Web:

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

Eseguire quindi dotnet-stack per visualizzare le tracce dello stack di thread:

dotnet-stack report -n DiagnosticScenarios

Verrà visualizzato un output lungo contenente un numero elevato di stack, molti dei quali hanno un aspetto simile al seguente:

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

I frame nella parte inferiore di questi stack indicano che questi thread sono thread ThreadPool:

  System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
  System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()

E i fotogrammi nella parte superiore rivelano che il thread è bloccato in una chiamata a GetResultCore(bool) dalla funzione 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()

Diagnosticare un problema intermittente con dotnet-trace

L'approccio dotnet-stack è efficace solo per operazioni di blocco ovvie e coerenti che si verificano in ogni richiesta. In alcuni scenari, il blocco si verifica sporadicamente solo ogni pochi minuti, rendendo dotnet-stack meno utile per la diagnosi del problema. In questo caso, è possibile usare dotnet-trace per raccogliere gli eventi in un periodo di tempo e salvarli in un file nettrace che può essere analizzato in un secondo momento.

C'è un evento specifico che aiuta a diagnosticare l'esaurimento del pool di thread: l'evento WaitHandleWait, introdotto in .NET 9. Viene emesso quando un thread viene bloccato da operazioni come chiamate sincrone su asincrono (ad esempio, Task.Result, Task.Wait, Task.GetAwaiter().GetResult()) o da altre operazioni di blocco come lock, Monitor.Enter, ManualResetEventSlim.Wait, e SemaphoreSlim.Wait.

Eseguire di nuovo Bombardier per caricare il server Web:

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

Eseguire quindi dotnet-trace per raccogliere gli eventi di attesa:

dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30

Deve generare un file denominato DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace contenente gli eventi. Questa nettrace può essere analizzata utilizzando due diversi strumenti:

Le sezioni seguenti illustrano come usare ogni strumento per leggere il file nettrace.

Analizzare un nettrace con Perfview

  1. Scaricare PerfView ed eseguirlo.

  2. Aprire il file nettrace facendo doppio clic su di esso.

    Screenshot dell'apertura di un nettrace in PerfView

  3. Fare doppio clic su Gruppo Avanzato>Qualsiasi Stack. Si apre una nuova finestra.

    Screenshot delle visualizzazioni qualsiasi stack in PerfView.

  4. Fare doppio clic sulla voce "Evento Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".

    Verranno ora visualizzate le tracce dello stack in cui sono stati generati gli eventi WaitHandleWait. Vengono suddivisi in base a "WaitSource". Attualmente sono disponibili due origini: MonitorWait per gli eventi generati tramite Monitor.Wait e Unknown per tutti gli altri.

    Screenshot della visualizzazione di qualsiasi pila per gli eventi di attesa in PerfView.

  5. Iniziare con MonitorWait perché rappresenta 64.8% degli eventi. È possibile selezionare le caselle di controllo per espandere le tracce dello stack responsabili dell'emissione di questo evento.

    Screenshot della visualizzazione in pila espansa per gli eventi di attesa in PerfView.

    Questa traccia dello stack può essere letta come: Task<T>.Result ha emesso un evento WaitHandleWait con un WaitSource MonitorWait (Task<T>.Result usa Monitor.Wait per eseguire un'attesa). È stato chiamato da DiagScenarioController.TaskWait, che è stato chiamato da alcune espressioni lambda, che è stato chiamato da alcuni ASP.NET codice

Analizzare un nettrace con il Visualizzatore Eventi .NET

  1. Passare a verdie-g.github.io/dotnet-events-viewer.

  2. Trascina e rilascia il file nettrace.

    Screenshot dell'apertura di un file di nettrace nel visualizzatore eventi .NET.

  3. Passare alla pagina Albero eventi , selezionare l'evento "WaitHandleWaitStart" e quindi selezionare Esegui query.

    Screenshot di una query sugli eventi nel Visualizzatore eventi .NET.

  4. Verranno visualizzate le tracce dello stack in cui sono stati generati gli eventi WaitHandleWait. Fare clic sulle frecce per espandere le tracce dello stack responsabili dell'emissione di questo evento.

    Screenshot della vista ad albero nel Visualizzatore eventi .NET.

    Questa traccia dello stack può essere letta come: ManualResetEventSlim.Wait ha generato un evento WaitHandleWait. È stato chiamato da Task.SpinThenBlockWait, che è stato chiamato da Task.InternalWaitCore, che è stato chiamato da Task<T>.Result, che è stato chiamato da DiagScenario.TaskWait, che è stato chiamato da alcune espressioni lambda, che è stato chiamato da del codice ASP.NET

Negli scenari reali, potresti trovare molti eventi di attesa generati da thread esterni al pool di thread. In questo caso, si sta indagando un thread pool esaurito, quindi tutte le attese su un thread dedicato all'esterno del pool di thread non sono rilevanti. Si può determinare se una traccia dello stack proviene da un thread del pool esaminando i primi metodi, che devono contenere una menzione del pool di thread (ad esempio, WorkerThread.WorkerThreadStart o ThreadPoolWorkQueue).

Parte superiore di una traccia dello stack del pool di thread.

Correzione del codice

È ora possibile navigare al codice di questo controller nel file Controllers/DiagnosticScenarios.cs dell'app di esempio, per vedere che sta chiamando un'API asincrona senza usare await. Si tratta del modello di codice sync-over-async, noto per il blocco dei thread ed è la causa più comune dello "starvation" del ThreadPool.

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

In questo caso, il codice può essere facilmente modificato per usare l'async/await, come illustrato nell'endpoint TaskAsyncWait() . L'uso di await consente al thread corrente di gestire altri elementi di lavoro mentre la query di database è in corso. Al termine della ricerca del database, un thread ThreadPool riprenderà l'esecuzione. In questo modo non viene bloccato alcun thread nel codice durante ogni richiesta.

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

L'esecuzione di Bombadier per inviare il carico all'endpoint api/diagscenario/taskasyncwait indica che il numero di thread ThreadPool rimane molto inferiore e la latenza media rimane vicina a 500 ms quando si usa l'approccio asincrono/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