Not
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Den här artikeln gäller för: ✔️ .NET 9.0 och senare versioner
I den här handledningen kommer du att lära dig hur man felsöker ett ThreadPool-svältscenario. ThreadPool-svält inträffar när poolen inte har några tillgängliga trådar för att bearbeta nya arbetsobjekt och ofta får program att svara långsamt. Med hjälp av det angivna exemplet ASP.NET Core-webbappen kan du avsiktligt orsaka ThreadPool-svält och lära dig hur du diagnostiserar den.
I den här handledningen kommer du att:
- Undersöka en app som svarar långsamt på begäranden
- Använd verktyget dotnet-counters för att identifiera att ThreadPool-svält sannolikt inträffar
- Använd dotnet-stack- och dotnet-trace-verktygen för att avgöra vilket arbete som håller ThreadPool-trådarna upptagna
Förutsättningar
Instruktionen använder:
- .NET 9 SDK för att skapa och köra exempelappen
- Exempelwebbapp för att demonstrera ThreadPool-svältbeteende
- Bombardier för att generera belastning för exempelwebbappen
- dotnet-räknare för att observera prestandaräknare
- dotnet-stack för att undersöka trådstackar
- dotnet-trace för att samla in väntehändelser
- Valfritt: PerfView för att analysera väntehändelserna
Kör exempelappen
Ladda ned koden för exempelappen och kör den med hjälp av .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
Om du använder en webbläsare och skickar begäranden till https://localhost:5001/api/diagscenario/taskwaitbör svaret success:taskwait returneras efter cirka 500 ms. Detta visar att webbservern hanterar trafik som förväntat.
Observera långsamma prestanda
Demowebbservern har flera slutpunkter som simulerar en databasförfrågan och sedan returnerar ett svar till användaren. Var och en av dessa slutpunkter har en fördröjning på cirka 500 ms när begäranden hanteras en i taget, men prestandan är mycket sämre när webbservern utsätts för viss belastning. Ladda ned testverktyget för Bombardier-belastning och observera skillnaden i svarstid när 125 samtidiga begäranden skickas till varje slutpunkt.
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
Den andra slutpunkten använder ett kodmönster som presterar ännu sämre:
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
Båda dessa slutpunkter visar dramatiskt mer än den genomsnittliga svarstiden på 500 ms när belastningen är hög (3,48 s respektive 15,42 s). Om du kör det här exemplet på en äldre version av .NET Core ser du förmodligen att båda exemplen fungerar lika dåligt. .NET 6 har uppdaterat ThreadPool-heuristiken som minskar prestandapåverkan för det dåliga kodningsmönstret som används i det första exemplet.
Identifiera ThreadPool-svält
Om du observerade beteendet ovan på en verklig tjänst skulle du veta att det svarar långsamt under belastning men du vet inte orsaken. dotnet-counters är ett verktyg som kan visa liveprestandaräknare. Dessa räknare kan ge ledtrådar om vissa problem och är ofta lätta att få. I produktionsmiljöer kan du ha liknande räknare som tillhandahålls av fjärrövervakningsverktyg och webbgränssnitt. Installera dotnet-counters och börja övervaka webbtjänsten:
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
Om din app kör en version av .NET som är äldre än .NET 9 ser utdatagränssnittet för dotnet-counters något annorlunda ut. se dotnet-counters för mer information.
Ovanstående räknare är ett exempel medan webbservern inte betjänade några begäranden. Kör Bombardier igen med api/diagscenario/tasksleepwait slutpunkten och ihållande belastning i 2 minuter så det finns gott om tid att se vad som händer med prestandaräknarna.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
ThreadPool-svält uppstår när det inte finns några lediga trådar för att hantera de köade arbetsobjekten och miljön svarar genom att öka antalet ThreadPool-trådar. Värdet dotnet.thread_pool.thread.count ökar snabbt till 2–3 gånger antalet processorkärnor på datorn och sedan läggs ytterligare trådar till 1–2 per sekund tills de stabiliseras någonstans över 125. De viktigaste signalerna om att ThreadPool-svält för närvarande är en flaskhals för prestanda är den långsamma och stadiga ökningen av ThreadPool-trådar och CPU-användning mycket mindre än 100%. Trådantalet ökar tills antingen poolen når det maximala antalet trådar, så många trådar som behövs har skapats så att alla inkommande arbetsobjekt kan hanteras eller tills processorn har mättats. Ofta, men inte alltid, visar ThreadPool-svält också stora värden för dotnet.thread_pool.queue.length och låga värden för dotnet.thread_pool.work_item.count, vilket innebär att det finns en stor mängd väntande arbete och lite arbete som slutförs. Här är ett exempel på räknarna medan antalet trådar fortfarande ökar:
[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
När antalet ThreadPool-trådar har stabiliserats svälter inte poolen längre. Men om den stabiliseras med ett högt värde (mer än ungefär tre gånger så många processorkärnor) indikerar det vanligtvis att programkoden blockerar vissa ThreadPool-trådar och ThreadPool kompenseras genom att köras med fler trådar. Att köra stabilt vid höga trådantal har inte nödvändigtvis stor inverkan på svarstiden för begäranden, men om belastningen varierar dramatiskt över tid eller om appen startas om regelbundet, kommer threadpoolen sannolikt att gå in i en period av svält där den långsamt ökar trådarna och ger dålig svarstid för begäranden. Varje tråd förbrukar också minne, så att minska det totala antalet trådar som behövs ger en annan fördel.
Från och med .NET 6 ändrades ThreadPool-heuristik för att skala upp antalet ThreadPool-trådar mycket snabbare som svar på vissa blockerande aktivitets-API:er. ThreadPool-svält kan fortfarande inträffa med dessa API:er, men varaktigheten är mycket kortare än med äldre .NET-versioner eftersom körtiden svarar snabbare. Kör Bombardier igen med slutpunkten api/diagscenario/taskwait.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
På .NET 6 bör du observera att poolen ökar antalet trådar snabbare än tidigare och sedan stabiliseras vid ett stort antal trådar. ThreadPool svälter medan trådantalet ökar.
Åtgärda ThreadPool-resursbrist
För att eliminera ThreadPool-svält måste ThreadPool-trådar förbli avblockerade så att de är tillgängliga för att hantera inkommande arbetsobjekt. Det finns flera sätt att avgöra vad varje tråd gjorde. Om problemet bara uppstår ibland är det bäst att samla in en spårning med dotnet-trace för att registrera programbeteende under en viss tidsperiod. Om problemet uppstår hela tiden kan du använda dotnet-stack-verktyget eller avbilda en dump med dotnet-dump som kan visas i Visual Studio. dotnet-stack kan vara snabbare eftersom den visar trådstackarna direkt på konsolen. Men Felsökning av Visual Studio-dumpar ger bättre visualiseringar som mappar ramar till källan, Just My Code kan filtrera bort körningsimplementeringsramar och funktionen Parallella staplar kan hjälpa till att gruppera ett stort antal trådar med liknande staplar. Den här handledningen visar alternativen dotnet-stack och dotnet-trace. Ett exempel på hur du undersöker trådstackarna med hjälp av Visual Studio finns i instruktionsvideon om att diagnostisera ThreadPool-svält.
Diagnostisera ett kontinuerligt problem med dotnet-stack
Kör Bombardier igen för att belastningssätta webbservern:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Kör sedan dotnet-stack för att se trådstackens spårningar:
dotnet-stack report -n DiagnosticScenarios
Du bör se långa utdata som innehåller ett stort antal staplar, varav många ser ut så här:
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()
Bildrutorna längst ned i dessa staplar anger att dessa trådar är ThreadPool-trådar:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
Och bildrutorna längst upp visar att tråden blockeras vid ett anrop till GetResultCore(bool) från funktionen 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()
Diagnostisera ett tillfälligt problem med dotnet-trace
Metoden dotnet-stack är endast effektiv för uppenbara, konsekventa blockeringsåtgärder som utförs i varje begäran. I vissa scenarier sker blockeringen sporadiskt bara med några minuters mellanrum, vilket gör dotnet-stack mindre användbar för att diagnostisera problemet. I det här fallet kan du använda dotnet-trace för att samla in händelser under en viss tidsperiod och spara dem i en nettrace-fil som kan analyseras senare.
Det finns en viss händelse som hjälper till att diagnostisera trådpoolssvält: händelsen WaitHandleWait, som introducerades i .NET 9. Den genereras när en tråd blockeras av åtgärder som synkronisering över asynkrona anrop (till exempel Task.Result, , och Task.Wait) eller av andra låsningsåtgärder som Task.GetAwaiter().GetResult(), lock, Monitor.Enteroch ManualResetEventSlim.WaitSemaphoreSlim.Wait.
Kör Bombardier igen för att belastningssätta webbservern:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Kör sedan dotnet-trace för att samla in väntehändelser:
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
Det bör generera en fil med namnet DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace som innehåller händelserna. Denna nettrace kan analyseras med två olika verktyg:
- PerfView: Ett verktyg för prestandaanalys som utvecklats av Microsoft endast för Windows.
- .NET Events Viewer: Ett webbverktyg för nettrace-analys i Blazor som utvecklats av communityn.
Följande avsnitt visar hur du använder varje verktyg för att läsa nettrace-filen.
Analysera en nettrace med Perfview
Ladda ned PerfView och kör den.
Öppna nettrace-filen genom att dubbelklicka på den.
Dubbelklicka på Avancerad grupp>Alla staplar. Ett nytt fönster öppnas.
Dubbelklicka på raden "Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".
Nu bör du se stackspårningarna där WaitHandleWait-händelserna har genererats. De är uppdelade efter "WaitSource". För närvarande finns det två källor:
MonitorWaitför händelser som genereras via Monitor.Wait ochUnknownför alla andra.Börja med MonitorWait eftersom det representerar 64,8% av händelserna. Du kan markera kryssrutorna för att expandera de stackspårningar som är ansvariga för att generera den här händelsen.
Den här stackspårningen kan läsas som:
Task<T>.Resulthar genererat en WaitHandleWait-händelse med en WaitSource MonitorWait (Task<T>.ResultanvänderMonitor.Waitför att utföra en vänt). Det kallades avDiagScenarioController.TaskWait, som kallades av någon lambda-funktion, som kallades av någon ASP.NET-kod
Analysera en nettrace med .NET Events Viewer
Dra och släpp nettrace-filen.
Gå till sidan Händelseträd , välj händelsen "WaitHandleWaitStart" och välj sedan Kör fråga.
Du bör se stackspårningarna där WaitHandleWait-händelserna har genererats. Klicka på pilarna för att expandera de stackspårningar som är ansvariga för att generera den här händelsen.
Den här stackspårningen kan läsas som:
ManualResetEventSlim.Waitutlöstes en WaitHandleWait-händelse. Det kallades avTask.SpinThenBlockWait, som kallades avTask.InternalWaitCore, som kallades avTask<T>.Result, som kallades avDiagScenario.TaskWait, som kallades av , som kallades av någon lambda, som kallades av någon ASP.NET kod
I verkliga scenarier kan du hitta många väntehändelser som genereras från trådar utanför trådpoolen. Här undersöker du en överbelastning av trådpoolen, så alla väntetider på dedikerade trådar utanför trådpoolen är inte relevanta. Du kan se om en stackspårning kommer från en trådpoolstråd genom att titta på de första metoderna, som bör innehålla ett omnämnande av trådpoolen (till exempel WorkerThread.WorkerThreadStart eller ThreadPoolWorkQueue).
Kodkorrigering
Nu kan du navigera till koden för den här kontrollanten i exempelappens controllers/DiagnosticScenarios.cs-fil för att se att den anropar ett asynkront API utan att använda await. Det här är kodmönstret sync-over-async , som är känt för att blockera trådar och är den vanligaste orsaken till ThreadPool-svält.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
I det här fallet kan koden enkelt ändras för att använda async/await i stället enligt vad som visas i TaskAsyncWait() slutpunkten. Med await kan den aktuella tråden betjäna andra arbetsytor medan databasfrågan pågår. När databassökningen är klar återupptas körningen av en ThreadPool-tråd. På så sätt blockeras ingen tråd i koden under varje begäran.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
När Du kör Bombadier för att skicka belastning till api/diagscenario/taskasyncwait slutpunkten visas att antalet ThreadPool-trådar förblir mycket lägre och att den genomsnittliga svarstiden förblir nära 500 ms när du använder metoden 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