Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Cet article s’applique à : ✔️ .NET 9.0 et versions ultérieures
Dans ce tutoriel, vous allez apprendre à déboguer un scénario de famine de ThreadPool. La famine de ThreadPool se produit lorsque le pool n’a pas de threads disponibles pour traiter de nouveaux éléments de travail et qu’il provoque souvent une réponse lente des applications. À l’aide de l’exemple fourni application web ASP.NET Core, vous pouvez provoquer intentionnellement l'épuisement du ThreadPool et apprendre comment le diagnostiquer.
Dans ce tutoriel, vous allez :
- Examiner une application qui répond aux demandes lentement
- Utiliser l’outil dotnet-counters pour identifier si un épuisement du ThreadPool se produit probablement.
- Utilisez les outils dotnet-stack et dotnet-trace pour déterminer quel travail consiste à maintenir les threads ThreadPool occupés
Conditions préalables
Le tutoriel utilise :
- Kit de développement logiciel (SDK) .NET 9 pour générer et exécuter l’exemple d’application
- Exemple d’application web pour illustrer le comportement de faim de ThreadPool
- Bombardier pour générer la charge pour l'application web d'exemple
- dotnet-counters pour observer les compteurs de performances
- dotnet-stack pour examiner les piles de threads
- dotnet-trace pour collecter les événements d’attente
- Facultatif : PerfView pour analyser les événements d’attente
Exécuter l’exemple d’application
Téléchargez le code de l’exemple d’application et exécutez-le à l’aide du Kit de développement logiciel (SDK) .NET :
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
Si vous utilisez un navigateur web et envoyez des demandes à https://localhost:5001/api/diagscenario/taskwait
, vous devez voir la réponse success:taskwait
retournée après environ 500 ms. Cela montre que le serveur web sert le trafic comme prévu.
Observer la lenteur du système
Le serveur web de démonstration a plusieurs points de terminaison qui simulent une requête sur la base de données, puis retournent une réponse à l’utilisateur. Chacun de ces points de terminaison a un délai d’environ 500 ms lors du traitement des requêtes un par un, mais les performances sont beaucoup plus mauvaises lorsque le serveur web est soumis à une certaine charge. Téléchargez l’outil de test de charge Bombardier et observez la différence de latence lorsque 125 requêtes simultanées sont envoyées à chaque point de terminaison.
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
Ce deuxième point de terminaison utilise un modèle de code qui s’exécute encore plus mal :
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
Ces deux points de terminaison affichent considérablement plus que la latence moyenne de 500 ms lorsque la charge est élevée (3,48 s et 15,42 s respectivement). Si vous exécutez cet exemple sur une version antérieure de .NET Core, vous pouvez voir que les deux exemples fonctionnent de manière égale. .NET 6 a mis à jour les heuristiques du ThreadPool qui réduisent l’impact sur les performances de la mauvaise pratique de codage utilisée dans le premier exemple.
Détecter la faim de ThreadPool
Si vous avez observé le comportement ci-dessus sur un service réel, vous savez qu’il répond lentement sous charge, mais vous ne connaissez pas la cause. dotnet-counters est un outil qui peut afficher des compteurs de performances en direct. Ces compteurs peuvent fournir des indices sur certains problèmes et sont souvent faciles à obtenir. Dans les environnements de production, vous pouvez avoir des compteurs similaires fournis par les outils de supervision à distance et les tableaux de bord web. Installez dotnet-counters et commencez à surveiller le service 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
Les compteurs précédents sont un exemple alors que le serveur web ne servait aucune requête. Réexécutez Bombardier avec le api/diagscenario/tasksleepwait
point de terminaison et la charge soutenue pendant 2 minutes afin qu’il y ait suffisamment de temps pour observer ce qui arrive aux compteurs de performances.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
La famine de ThreadPool se produit lorsqu’il n’y a pas de threads libres pour gérer les éléments de travail mis en file d’attente et que le runtime répond en augmentant le nombre de threads ThreadPool. La dotnet.thread_pool.thread.count
valeur augmente rapidement à 2 à 3 fois le nombre de cœurs de processeur sur votre machine, puis d’autres threads sont ajoutés de 1 à 2 par seconde jusqu’à ce qu’ils se stabilisent quelque part au-dessus de 125. Les signaux clés indiquant que l'épuisement du ThreadPool constitue actuellement un goulot d’étranglement des performances sont l’augmentation lente et régulière des threads ThreadPool et une utilisation du processeur bien inférieure à 100%. L'augmentation du nombre de threads se poursuivra jusqu'à ce que le pool atteigne son nombre maximal de threads, que suffisamment de threads aient été créés pour satisfaire tous les éléments de travail entrants, ou que le processeur soit saturé. Souvent, mais pas toujours, la saturation du ThreadPool affiche également de grandes valeurs pour dotnet.thread_pool.queue.length
et des valeurs faibles pour dotnet.thread_pool.work_item.count
, ce qui signifie qu’il y a une grande quantité de travail en attente et peu de travail accompli. Voici un exemple de compteurs alors que le nombre de threads est toujours en hausse :
[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
Une fois le nombre de threads ThreadPool stabilisés, le pool n’est plus affamé. Mais s’il se stabilise à une valeur élevée (plus de trois fois le nombre de cœurs de processeur), cela indique généralement que le code de l’application bloque certains threads ThreadPool et que threadPool est compensé par l’exécution avec plus de threads. L’exécution stable au nombre élevé de threads n’aura pas nécessairement d’impact important sur la latence des requêtes, mais si la charge varie considérablement au fil du temps ou que l’application sera régulièrement redémarrée, chaque fois que le ThreadPool est susceptible d’entrer une période de faim où il augmente lentement les threads et fournit une latence de requête médiocre. Chaque thread consomme également de la mémoire, de sorte que la réduction du nombre total de threads nécessaires offre un autre avantage.
À compter de .NET 6, les heuristiques threadPool ont été modifiées pour augmenter le nombre de threads ThreadPool beaucoup plus rapidement en réponse à certaines API de tâche bloquantes. La famine de ThreadPool peut toujours se produire avec ces API, mais la durée est beaucoup plus courte que celle des versions plus anciennes de .NET, car le runtime répond plus rapidement. Réexécutez à nouveau Bombardier avec le point de terminaison api/diagscenario/taskwait
.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Sur .NET 6, vous devez observer que le pool augmente le nombre de threads plus rapidement qu’avant, puis stabiliser à un nombre élevé de threads. La famine de ThreadPool se produit pendant l’escalade du nombre de threads.
Résoudre la faim de ThreadPool
Pour éliminer la saturation du ThreadPool, les threads doivent rester débloqués afin d'être disponibles pour gérer les éléments de travail entrants. Il existe plusieurs façons de déterminer ce que faisait chaque thread. Si le problème se produit uniquement occasionnellement, la collecte d’une trace avec dotnet-trace est préférable pour enregistrer le comportement de l’application sur une période donnée. Si le problème se produit constamment, vous pouvez utiliser l’outil dotnet-stack ou capturer un vidage avec dotnet-dump qui peut être affiché dans Visual Studio. dotnet-stack peut être plus rapide, car il affiche les piles de threads immédiatement sur la console. Toutefois, le débogage de vidage dans Visual Studio offre de meilleures visualisations qui associent les cadres à la source, Juste mon code peut filtrer les cadres d'implémentation du runtime, et la fonctionnalité Stacks parallèles peut aider à regrouper de nombreux threads possédant des piles similaires. Ce tutoriel présente les options dotnet-stack et dotnet-trace. Pour obtenir un exemple d’examen des piles de threads à l’aide de Visual Studio, consultez la vidéo du didacticiel sur le diagnostic de la saturation du ThreadPool.
Diagnostiquer un problème continu avec dotnet-stack
Réexécutez Bombardier pour mettre le serveur web en charge :
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Exécutez ensuite dotnet-stack pour afficher les traces de la pile de threads :
dotnet-stack report -n DiagnosticScenarios
Vous devriez voir un long résultat contenant un grand nombre de piles, dont beaucoup ont une apparence similaire à celle-ci :
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()
Les cadres en bas de ces piles indiquent que ces threads sont des threads du pool de threads :
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
Et les trames situées près du haut révèlent que le thread est bloqué sur un appel à GetResultCore(bool)
provenant de la fonction 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()
Diagnostiquer un problème intermittent avec dotnet-trace
L’approche dotnet-stack est efficace uniquement pour les opérations de blocage évidentes et cohérentes qui se produisent dans chaque requête. Dans certains scénarios, le blocage se produit sporadiquement toutes les quelques minutes, ce qui rend dotnet-stack moins utile pour diagnostiquer le problème. Dans ce cas, vous pouvez utiliser dotnet-trace pour collecter des événements sur une période donnée et les enregistrer dans un fichier nettrace qui peut être analysé ultérieurement.
Il existe un événement particulier qui permet de diagnostiquer la saturation du pool de threads : l’événement WaitHandleWait, qui a été introduit dans .NET 9. Il est émis lorsqu'un thread devient bloqué par des opérations telles que des appels synchrones sur appels asynchrones (par exemple, Task.Result
, Task.Wait
, et Task.GetAwaiter().GetResult()
) ou par d'autres opérations de verrouillage telles que lock
, Monitor.Enter
, ManualResetEventSlim.Wait
, et SemaphoreSlim.Wait
.
Réexécutez Bombardier pour mettre le serveur web en charge :
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Exécutez ensuite dotnet-trace pour collecter les événements d’attente :
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
Cela doit générer un fichier nommé DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace
contenant les événements. Ce nettrace peut être analysé à l’aide de deux outils différents :
- PerfView : outil d’analyse des performances développé par Microsoft pour Windows uniquement.
- Visionneuse d’événements .NET : outil web Blazor d’analyse nettrace développé par la communauté.
Les sections suivantes montrent comment utiliser chaque outil pour lire le fichier nettrace.
Analyser un nettrace avec Perfview
Téléchargez PerfView et exécutez-le.
Ouvrez le fichier nettrace en double-cliquant dessus.
Double-cliquez sur Advanced Group>Any Stacks. Une nouvelle fenêtre s’ouvre.
Double-cliquez sur la ligne « Événement Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start ».
Vous devriez maintenant voir les traces de pile où les événements WaitHandleWait ont été émis. Ils sont divisés par « WaitSource ». Actuellement, il existe deux sources :
MonitorWait
pour les événements émis via Monitor.Wait etUnknown
pour tous les autres.Commencez par MonitorWait, car il représente 64,8% des événements. Vous pouvez cocher les cases pour développer les traces de pile responsables de l’émission de cet événement.
Cette trace de pile peut être interprétée comme suit :
Task<T>.Result
a émis un événement WaitHandleWait avec un WaitSource MonitorWait (Task<T>.Result
utiliseMonitor.Wait
pour effectuer une attente). Il a été appelé parDiagScenarioController.TaskWait
, qui a été appelé par une fonction lambda, qui a été appelée par du code ASP.NET
Analyser un nettrace avec l’observateur d’événements .NET
Accédez à verdie-g.github.io/dotnet-events-viewer.
Faites glisser-déplacer le fichier nettrace.
Accédez à la page Arborescence des événements , sélectionnez l’événement « WaitHandleWaitStart », puis sélectionnez Exécuter la requête.
Vous devez voir les traces de pile où les événements WaitHandleWait ont été émis. Cliquez sur les flèches pour développer les traces de pile responsables de l’émission de cet événement.
Cette trace de pile peut être lue comme suit :
ManualResetEventSlim.Wait
a émis un événement WaitHandleWait. Il a été appelé parTask.SpinThenBlockWait
, qui a été appelé parTask.InternalWaitCore
, qui a été appelé parTask<T>.Result
, qui a été appelé parDiagScenario.TaskWait
, qui a été appelé par une fonction lambda, qui a été appelé par du code ASP.NET
Dans les scénarios réels, vous pouvez trouver beaucoup d’événements d’attente émis à partir de threads en dehors du pool de threads. Dans ce contexte, vous examinez un épuisement du pool de threads, donc les attentes sur un thread dédié en dehors du pool de threads ne sont pas pertinentes. Vous pouvez déterminer si une trace de pile d'exécution provient d'un fil de pool de threads en examinant les premières méthodes, qui devraient mentionner le pool de threads (par exemple, WorkerThread.WorkerThreadStart
ou ThreadPoolWorkQueue
).
Correction de code
Vous pouvez maintenant accéder au code de ce contrôleur dans le fichier Contrôleurs/DiagnosticScenarios.cs de l’exemple d’application pour voir qu’il appelle une API asynchrone sans utiliser await
. Il s'agit du modèle de code sync-over-async, qui est connu pour bloquer les threads et est la cause la plus courante de l'épuisement du ThreadPool.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
Dans ce cas, le code peut être facilement modifié pour utiliser l’async/await à la place, comme indiqué dans le TaskAsyncWait()
point de terminaison. L’utilisation d’await permet au thread actuel de traiter d’autres éléments de travail pendant que la requête de base de données est en cours. Une fois la recherche de base de données terminée, un thread ThreadPool reprend l’exécution. De cette façon, aucun thread n’est bloqué dans le code pendant chaque requête.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
L’exécution de Bombadier pour envoyer la api/diagscenario/taskasyncwait
charge au point de terminaison montre que le nombre de threads ThreadPool reste beaucoup plus faible et que la latence moyenne reste proche de 500 ms lors de l’utilisation de l’approche asynchrone/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