Dela via


Felsöka ThreadPool-utmattning

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:

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

  1. Ladda ned PerfView och kör den.

  2. Öppna nettrace-filen genom att dubbelklicka på den.

    Skärmbild av öppnandet av en nettrace i PerfView

  3. Dubbelklicka på Avancerad grupp>Alla staplar. Ett nytt fönster öppnas.

    Skärmbild av vyn

  4. 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: MonitorWait för händelser som genereras via Monitor.Wait och Unknown för alla andra.

    Skärmbild av vyn för eventuella staplar för väntehändelser i PerfView.

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

    Skärmbild av den expanderade stackvyn för väntehändelser i PerfView.

    Den här stackspårningen kan läsas som: Task<T>.Result har genererat en WaitHandleWait-händelse med en WaitSource MonitorWait (Task<T>.Result använder Monitor.Wait för att utföra en vänt). Det kallades av DiagScenarioController.TaskWait, som kallades av någon lambda-funktion, som kallades av någon ASP.NET-kod

Analysera en nettrace med .NET Events Viewer

  1. Gå till verdie-g.github.io/dotnet-events-viewer.

  2. Dra och släpp nettrace-filen.

    Skärmbild av öppningen av en nettrace i .NET Events Viewer.

  3. Gå till sidan Händelseträd , välj händelsen "WaitHandleWaitStart" och välj sedan Kör fråga.

    Skärmbild av en händelsefråga i .NET-loggboken.

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

    Skärmbild av trädvyn i .NET Händelsevisaren.

    Den här stackspårningen kan läsas som: ManualResetEventSlim.Wait utlöstes en WaitHandleWait-händelse. Det kallades av Task.SpinThenBlockWait, som kallades av Task.InternalWaitCore, som kallades av Task<T>.Result, som kallades av DiagScenario.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).

Toppen av stackspårningen i en tråd från trådpoolen.

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