Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Tento článek se vztahuje na: ✔️ .NET 9.0 a novější verze
V tomto kurzu se dozvíte, jak ladit scénář hladovění fondu vláken. Vyčerpání fondu vláken nastává, když fond nemá k dispozici žádné vlákna pro zpracování nových pracovních položek, což často způsobuje, že aplikace reagují pomalu. Pomocí poskytnuté ukázkové webové aplikace ASP.NET Core můžete záměrně způsobit hladovění ThreadPoolu a zjistit, jak ji diagnostikovat.
V tomto kurzu:
- Prozkoumání aplikace, která reaguje na požadavky pomalu
- Pomocí nástroje dotnet-counters identifikujte, že pravděpodobně dochází k hladovění fondu vláken.
- Pomocí nástrojů dotnet-stack a dotnet-trace určete, jaká práce udržuje vlákna ThreadPool zaneprázdněna.
Požadavky
Tutoriál používá:
- .NET 9 SDK k sestavení a spuštění ukázkové aplikace
- Ukázková webová aplikace pro předvedení, jak funguje hladovění ve vláknovém fondu
- Bombardier pro generování zatížení pro ukázkovou webovou aplikaci
- dotnet-counters ke sledování ukazatelů výkonu
- dotnet-stack se používá pro zkoumání zásobníků vláken
- dotnet-trace pro sběr čekajících událostí
- Volitelné: PerfView k analýze událostí čekání
Spuštění ukázkové aplikace
Stáhněte si kód ukázkové aplikace a spusťte ho pomocí sady .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
Pokud používáte webový prohlížeč a odesíláte požadavky na https://localhost:5001/api/diagscenario/taskwait, měla by se zobrazit odpověď vrácená z success:taskwait po přibližně 500 ms. To ukazuje, že webový server obsluhuje provoz podle očekávání.
Sledování pomalého výkonu
Ukázkový webový server má několik koncových bodů, které napodobují provedení požadavku na databázi a následně vrátí odpověď uživateli. Každý z těchto koncových bodů má zpoždění přibližně 500 ms při poskytování požadavků po jednom, ale výkon je mnohem horší, když webový server podléhá určité zátěži. Stáhněte si nástroj pro zátěžové testování Společnosti Bombardier a sledujte rozdíl v latenci při odesílání 125 souběžných požadavků do každého koncového bodu.
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
Tento druhý koncový bod používá vzor kódu, který provádí ještě horší výkon:
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
Oba tyto koncové body ukazují výrazně vyšší než průměrná latence 500 ms při vysokém zatížení (3,48 s a 15,42 s v uvedeném pořadí). Pokud tento příklad spustíte ve starší verzi .NET Core, pravděpodobně uvidíte, že oba příklady fungují stejně špatně. .NET 6 aktualizoval heuristika ThreadPool, která snižuje dopad na výkon chybného vzoru kódování použitého v prvním příkladu.
Zjištění hladovění fondu vláken
Pokud jste zaznamenali výše uvedené chování ve skutečné službě, víte, že reaguje pomalu při zatížení, ale neznáte příčinu. dotnet-counters je nástroj, který může zobrazit živé čítače výkonu. Tyto čítače mohou poskytnout vodítka k určitým problémům a často je snadné je získat. V produkčních prostředích můžete mít podobné čítače poskytované nástroji pro vzdálené monitorování a webovými řídicími panely. Nainstalujte dotnet-counters a začněte monitorovat webovou službu:
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
Pokud vaše aplikace používá verzi .NET starší než .NET 9, bude výstupní uživatelské rozhraní čítačů dotnet-counter vypadat trochu jinak; Podrobnosti najdete v čítačích dotnet. .
Předchozí čítače jsou příkladem pro situaci, kdy webový server nevyřizoval žádné žádosti. Znovu spusťte Bombardier s koncovým bodem api/diagscenario/tasksleepwait a udržovanou zátěží po dobu 2 minut, abyste měli dostatek času sledovat, co se stane s čítači výkonu.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
K vyčerpání fondu vláken dochází v případě, že neexistují žádná volná vlákna pro zpracování pracovních položek ve frontě a modul runtime reaguje zvýšením počtu vláken z fondu vláken. Hodnota dotnet.thread_pool.thread.count se rychle zvýší na 2 až 3x počet jader procesoru na vašem počítači a další vlákna se přidají 1 až 2 za sekundu, dokud se nestabiluje někde nad 125. Klíčové signály, že hladovění ThreadPool je v současné době kritickým bodem výkonu jsou pomalé a stabilní zvýšení vláken ThreadPool a využití procesoru mnohem méně než 100%. Zvýšení počtu vláken bude pokračovat, dokud fond nedosáhne maximálního počtu vláken, bylo vytvořeno dostatek vláken pro splnění všech příchozích pracovních položek nebo je procesor plně vytížen. Často, ale ne vždy, vyhladovění ThreadPool také zobrazí velké hodnoty pro dotnet.thread_pool.queue.length a nízké hodnoty pro dotnet.thread_pool.work_item.count, což znamená, že existuje velké množství čekající práce a málo dokončené práce. Tady je příklad čítačů, zatímco počet vláken stále roste:
[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
Jakmile se počet vláken ThreadPool stabilizuje, fond již nebude v nedostatku. Pokud se ale stabilizuje na vysoké hodnotě (více než přibližně třikrát větší než počet jader procesoru), obvykle to znamená, že kód aplikace blokuje některá vlákna ThreadPool a ThreadPool to kompenzuje spuštěním s více vlákny. Pokud vláknový fond běží stabilně při vysokém počtu vláken, nemusí to nutně mít velký dopad na latenci požadavků, ale pokud se zatížení výrazně v průběhu času liší nebo se aplikace bude pravidelně restartovat, pak se fond vláken pravděpodobně dostane do období hladovění, kdy pomalu zvyšuje počet vláken a způsobuje vysokou latenci požadavků. Každé vlákno také spotřebovává paměť, takže snížení celkového počtu potřebných vláken poskytuje další výhodu.
Počínaje rozhraním .NET 6 se heuristiky ThreadPool změnily tak, aby se počet vláken ThreadPool škáloval mnohem rychleji v reakci na určitá blokující rozhraní API úloh. K hladovění fondu vláken může stále docházet u těchto rozhraní API, ale doba trvání je mnohem kratší než u starších verzí .NET, protože modul runtime reaguje rychleji. Spusťte Bombardier znovu s koncovým bodem api/diagscenario/taskwait:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Na platformě .NET 6 byste měli sledovat, jak fond zvyšuje počet vláken rychleji než předtím a pak se stabilizuje při vysokém počtu vláken. Vyčerpání fondu vláken se vyskytuje, zatímco počet vláken stoupá.
Řešení hladovění fondu vláken
Aby se zabránilo hladovění vláknového fondu, musí vlákna ThreadPool zůstat odblokovaná, aby byla dostupná pro zpracování příchozích pracovních položek. Existuje několik způsobů, jak určit, co jednotlivá vlákna dělala. Pokud k problému dochází jen občas, je nejlepší zaznamenávat chování aplikace v průběhu času pomocí dotnet-trace pro shromažďování záznamů. Pokud k problému dochází neustále, můžete použít nástroj dotnet-stack nebo zachytit výpis pomocí dotnet-dump, který lze zobrazit v aplikaci Visual Studio. dotnet-stack může být rychlejší, protože okamžitě zobrazuje zásobníky vláken na konzoli. Ladění výpisu paměti sady Visual Studio ale nabízí lepší vizualizace, které mapují rámce na zdroj, just My Code dokáže vyfiltrovat rámce implementace modulu runtime a funkce Paralelní zásobníky může pomoct seskupit velký počet vláken s podobnými zásobníky. Tento kurz ukazuje možnosti dotnet-stack a dotnet-trace. Příklad zkoumání zásobníků vláken pomocí sady Visual Studio najdete v výukovém videu o diagnostice technologie ThreadPool.
Diagnostika neustálého problému s nástrojem dotnet-stack
Spuštěním příkazu Bombardier znovu umístěte webový server pod zatížení:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Pak spusťte dotnet-stack, abyste zobrazili výpisy zásobníků vláken.
dotnet-stack report -n DiagnosticScenarios
Měl by se zobrazit dlouhý výstup obsahující velký počet zásobníků, z nichž mnohé vypadají takto:
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()
Rámce v dolní části těchto zásobníků označují, že tato vlákna jsou vlákna ThreadPool:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
A rámce v horní části ukazují, že vlákno je blokováno při volání funkce DiagnosticScenarioController.TaskWait() do GetResultCore(bool):
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()
Diagnostikovat občasný problém s dotnet-trace
Přístup dotnet-stack je efektivní jenom pro běžné konzistentní blokující operace, ke kterým dochází v každém požadavku. V některých scénářích dochází k blokování sporadicky pouze každých několik minut, což snižuje užitečnost dotnet-stack pro diagnostiku problému. V tomto případě můžete použít dotnet-trace ke shromažďování událostí v určitém časovém období a jejich uložení do souboru nettrace, který lze analyzovat později.
Jedna konkrétní událost, která pomáhá diagnostikovat nedostatek vláken ve fondu, je událost WaitHandleWait, která byla představena v .NET 9. Vygeneruje se, když se vlákno zablokuje operacemi, jako jsou synchronní nad asynchronní volání (například Task.Result, Task.Wait a Task.GetAwaiter().GetResult()), nebo jinými operacemi uzamčení, jako jsou lock, Monitor.Enter, ManualResetEventSlim.Wait a SemaphoreSlim.Wait.
Spuštěním příkazu Bombardier znovu umístěte webový server pod zatížení:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Pak spuštěním příkazu dotnet-trace shromážděte události čekání:
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
Tím by se měl vygenerovat soubor s názvem DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace obsahující události. Tuto nettrace je možné analyzovat pomocí dvou různých nástrojů:
- PerfView: Nástroj pro analýzu výkonu vyvinutý microsoftem pouze pro Windows.
- Prohlížeč událostí .NET: Webový nástroj pro analýzu nettrace vyvinutý komunitou pomocí Blazor.
Následující části ukazují, jak pomocí jednotlivých nástrojů číst soubor nettrace.
Analýza nettrace pomocí nástroje Perfview
Stáhněte PerfView a spusťte ho.
Otevřete soubor nettrace tak, že na něj dvakrát kliknete.
Poklikejte na Rozšířené seskupení>Libovolné zásobníky. Otevře se nové okno.
Poklikejte na řádek „Událost Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start“.
Teď byste měli vidět zásobníkové stopy, kde byly události WaitHandleWait zaznamenány. Rozdělují se podle "WaitSource". V současné době existují dva zdroje:
MonitorWaitpro události generované prostřednictvím monitor.Wait aUnknownpro všechny ostatní.Začněte s monitorWait, protože představuje 64.8% událostí. Zaškrtnutím políček můžete rozbalit trasování zásobníku zodpovědného za vyvolání této události.
Toto trasování zásobníku lze číst jako:
Task<T>.Resultvygeneruje událost WaitHandleWait s waitSource MonitorWait (Task<T>.ResultpoužíváMonitor.Waitk provedení čekání). Bylo volánoDiagScenarioController.TaskWait, který byl volán nějakou lambda funkcí, kterou volal nějaký ASP.NET kód.
Analýza nettrace pomocí Prohlížeče událostí .NET
Přejděte na verdie-g.github.io/dotnet-events-viewer.
Přetáhněte soubor nettrace.
Přejděte na stránku Strom událostí , vyberte událost WaitHandleWaitStart a pak vyberte Spustit dotaz.
Měli byste vidět trasování zásobníku, kde se vygenerovaly události WaitHandleWait. Kliknutím na šipky rozbalíte trasování zásobníku zodpovědné za generování této události.
Toto trasování zásobníku lze číst jako:
ManualResetEventSlim.Waitvyvolalo událost typu WaitHandleWait.Task.SpinThenBlockWaitbylo volánoTask.InternalWaitCore, které bylo volánoTask<T>.Result, které bylo volánoDiagScenario.TaskWait, které bylo voláno nějakou lambda funkcí, která byla volána nějakým kódem ASP.NET.
Ve skutečných scénářích můžete najít velké množství událostí čekání vygenerovaných z vláken mimo fond vláken. Tady prošetřujete hladovění fondu vláken, takže všechna čekání na vyhrazených vláknech mimo fond vláken nejsou relevantní. Abyste zjistili, jestli stack trace pochází z vlákna z fondu vláken, podívejte se na první metody, které by měly pravděpodobně obsahovat zmínku o fondu vláken (například WorkerThread.WorkerThreadStart nebo ThreadPoolWorkQueue).
Oprava kódu
Teď můžete přejít na kód tohoto kontroleru v souboru Controllers/DiagnosticScenarios.cs ukázkové aplikace a zjistit, že volá asynchronní rozhraní API bez použití await. Toto je vzor synchronizace nad asynchronním, o kterém je známo, že blokuje vlákna a je nejčastější příčinou vyčerpaného vlákna ThreadPoolu.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
V takovém případě je možné kód snadno změnit, aby používal async/await, jak je znázorněno v TaskAsyncWait() endpointu. Použití operátoru await umožňuje aktuálnímu vláknu obsluhovat další pracovní položky, zatímco probíhá databázový dotaz. Po dokončení vyhledávání databáze bude vlákno ThreadPool pokračovat v provádění. Tímto způsobem není v kódu během každého požadavku blokováno žádné vlákno.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
Spuštění Bombadier pro odeslání zátěže do koncového bodu api/diagscenario/taskasyncwait ukazuje, že počet vláken v ThreadPool zůstává mnohem nižší a průměrná latence se při použití přístupu async/await pohybuje kolem 500 ms.
>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