Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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:
- .NET 9 SDK per compilare ed eseguire l'app di esempio
- Web app di esempio per illustrare il comportamento di saturazione del ThreadPool
- Bombardier per generare il carico per l'app Web di esempio
- dotnet-counters per osservare i contatori delle prestazioni
- dotnet-stack per esaminare gli stack di thread
- dotnet-trace per raccogliere eventi di attesa
- Facoltativo: PerfView per analizzare gli eventi di attesa
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:
- PerfView: strumento di analisi delle prestazioni sviluppato solo da Microsoft per Windows.
- Visualizzatore eventi .NET: strumento Web Blazor di analisi nettrace sviluppato dalla community.
Le sezioni seguenti illustrano come usare ogni strumento per leggere il file nettrace.
Analizzare un nettrace con Perfview
Scaricare PerfView ed eseguirlo.
Aprire il file nettrace facendo doppio clic su di esso.
Fare doppio clic su Gruppo Avanzato>Qualsiasi Stack. Si apre una nuova finestra.
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:
MonitorWaitper gli eventi generati tramite Monitor.Wait eUnknownper tutti gli altri.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.
Questa traccia dello stack può essere letta come:
Task<T>.Resultha emesso un evento WaitHandleWait con un WaitSource MonitorWait (Task<T>.ResultusaMonitor.Waitper eseguire un'attesa). È stato chiamato daDiagScenarioController.TaskWait, che è stato chiamato da alcune espressioni lambda, che è stato chiamato da alcuni ASP.NET codice
Analizzare un nettrace con il Visualizzatore Eventi .NET
Passare a verdie-g.github.io/dotnet-events-viewer.
Trascina e rilascia il file nettrace.
Passare alla pagina Albero eventi , selezionare l'evento "WaitHandleWaitStart" e quindi selezionare Esegui query.
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.
Questa traccia dello stack può essere letta come:
ManualResetEventSlim.Waitha generato un evento WaitHandleWait. È stato chiamato daTask.SpinThenBlockWait, che è stato chiamato daTask.InternalWaitCore, che è stato chiamato daTask<T>.Result, che è stato chiamato daDiagScenario.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).
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