Aracılığıyla paylaş


ThreadPool kaynak yetersizliğini giderme

Bu makale şunlar için geçerlidir: ✔️ .NET 9.0 ve üzeri sürümler

Bu öğreticide, bir ThreadPool açlık senaryosunu nasıl debug edeceğinizi öğreneceksiniz. ThreadPool sıkıntısı, havuzda yeni iş öğelerini işlemek için kullanılabilir iş parçacığı olmadığında yaşanır ve bu genellikle uygulamaların yanıt sürelerini yavaşlatır. Sağlanan örnek ASP.NET Core web uygulamasını kullanarak, ThreadPool'un açlıktan ölmesine neden olabilir ve bunu tanılamayı öğrenebilirsiniz.

Bu kılavuzda aşağıdakileri yapacaksınız:

  • İsteklere yavaş yanıt veren bir uygulamayı araştırma
  • ThreadPool'un aç kalmasının muhtemel olduğunu belirlemek için dotnet-counters aracını kullanın
  • dotnet-stack ve dotnet-trace araçlarını kullanarak ThreadPool iş parçacıklarını meşgul eden çalışmayı belirleyin

Önkoşullar

Eğiticide şunlar kullanılıyor:

  • Örnek uygulamayı derlemek ve çalıştırmak için .NET 9 SDK
  • ThreadPool açlıktan ölme davranışını göstermek için örnek web uygulaması
  • Örnek web uygulaması için yük oluşturmak için bombardier
  • performans sayaçlarını gözlemlemek için dotnet-counters
  • dotnet-stack iş parçacığı yığınlarını incelemek için kullanılır
  • bekleme olaylarını toplamak için dotnet-trace
  • İsteğe bağlı: Bekleme olaylarını analiz etmek için PerfView

Örnek uygulamayı çalıştırma

Örnek uygulamanın kodunu indirin ve .NET SDK'sını kullanarak çalıştırın:

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

Bir web tarayıcısı kullanıyor ve adresine https://localhost:5001/api/diagscenario/taskwaitistek gönderiyorsanız, yaklaşık 500 ms sonra döndürülen yanıtı success:taskwait görmeniz gerekir. Bu, web sunucusunun trafiğe beklendiği gibi hizmet ettiğini gösterir.

Yavaş performansı gözlemleme

Tanıtım web sunucusu, veritabanı isteğini taklit eden ve ardından kullanıcıya yanıt döndüren çeşitli uç noktalara sahiptir. Bu uç noktaların her biri, isteklere teker teker sunulurken yaklaşık 500 ms gecikmeye sahiptir, ancak web sunucusu bir miktar yüke maruz kaldığı zaman performans çok daha kötüdür. Bombardier yük testi aracını indirin ve her uç noktaya 125 eşzamanlı istek gönderildiğinde gecikme süresi farkını gözlemleyin.

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

Bu ikinci uç nokta daha da kötü performans gösteren bir kod deseni kullanır:

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

Bu uç noktaların her ikisi de yük yüksek olduğunda (sırasıyla 3,48 sn ve 15,42 sn) 500 ms ortalama gecikme süresinden önemli ölçüde fazladır. Bu örneği .NET Core'un daha eski bir sürümünde çalıştırırsanız, her iki örneğin de aynı şekilde kötü performans sergilediğini görebilirsiniz. .NET 6, ilk örnekte kullanılan hatalı kodlama deseninin performans etkisini azaltan ThreadPool buluşsal yöntemlerini güncelleştirdi.

ThreadPool açlığı algılama

Yukarıdaki davranışı gerçek bir dünya hizmetinde gözlemlediyseniz yük altında yavaş yanıt verdiğini bilirsiniz ancak nedenini bilmezsiniz. dotnet-counters , canlı performans sayaçlarını gösterebilen bir araçtır. Bu sayaçlar belirli sorunlar hakkında ipuçları sağlayabilir ve genellikle kolayca elde edilebilir. Üretim ortamlarında, uzaktan izleme araçları ve web panoları tarafından sağlanan benzer sayaçlarınız olabilir. dotnet-counters'ı yükleyin ve web hizmetini izlemeye başlayın:

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

Uygulamanız .NET 9'dan daha eski bir .NET sürümü çalıştırıyorsa dotnet-counters çıkış kullanıcı arabirimi biraz farklı görünür; Ayrıntılar için dotnet-counters bölümüne bakın.

Yukarıdaki sayaçlar, web sunucusu herhangi bir istek sunmazken bir örnektir. Bombardier'i api/diagscenario/tasksleepwait uç noktası ve sabit yük ile 2 dakika boyunca yeniden çalıştırın, böylece performans sayaçlarında ne olduğunu gözlemlemek için bol bol zamanınız olur.

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s

ThreadPool açlığı, kuyruğa alınan iş öğelerini işlemek için boş iş parçacığı olmadığında oluşur ve çalışma zamanı ThreadPool iş parçacığı sayısını artırarak yanıt verir. dotnet.thread_pool.thread.count değeri, makinenizdeki işlemci çekirdeği sayısının 2-3 katına hızla yükselir ve daha sonra 125'in üzerinde bir yere sabitlenene kadar saniyede 1-2 iş parçacığı eklenmeye devam eder. ThreadPool açlığının şu anda bir performans darboğazı olduğunu belirten önemli sinyaller, ThreadPool iş parçacıklarının yavaş ve kararlı artışı ile CPU Kullanımı'nın 100%'nin oldukça altında olmasıdır. İş parçacığı sayısı artışı, havuz iş parçacığı sayısı üst sınırına ulaşana, tüm gelen iş öğelerini karşılamak için yeterli iş parçacığı oluşturulana veya CPU doygunluğa ulaşana kadar devam eder. Genellikle, ancak her zaman değil, ThreadPool yetersizliği durumunda dotnet.thread_pool.queue.length için büyük ve dotnet.thread_pool.work_item.count için düşük değerler gösterilir; bu ise çok miktarda işin beklemede olduğunu ve az sayıda işin tamamlandığını ifade eder. İş parçacığı sayısı hala artarken sayaçlara bir örnek aşağıda verilmiştir:

[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

ThreadPool iş parçacıklarının sayısı dengeli hale geldikten sonra havuzda artık kaynak sıkıntısı yaşanmaz. Ancak yüksek bir değerde kararlı hale gelirse (işlemci çekirdeği sayısının yaklaşık üç katı), bu genellikle uygulama kodunun bazı ThreadPool iş parçacıklarını engellediğini ve ThreadPool'un daha fazla iş parçacığıyla çalıştırılarak telafi ettiğini gösterir. Yüksek iş parçacığı sayılarında sabit çalışmak, istek gecikme süresi üzerinde büyük bir etki yaratmayabilir, ancak yük zamanla önemli ölçüde değişirse veya uygulama belirli aralıklarla yeniden başlatılırsa, her seferinde ThreadPool'un iş parçacıklarını yavaşça artırarak yetersiz kaynak durumuna girip gecikme süresini kötüleştirmesi muhtemeldir. Her iş parçacığı bellek de tüketir, bu nedenle gereken toplam iş parçacığı sayısını azaltmak başka bir avantaj sağlar.

.NET 6'dan itibaren, ThreadPool sezgisel yöntemleri belirli engelleme Görev API'lerine yanıt olarak ThreadPool iş parçacığı sayısını çok daha hızlı artıracak şekilde değiştirildi. ThreadPool açlığı bu API'lerde yine de oluşabilir, ancak çalışma zamanı daha hızlı yanıt verdiği için süre eski .NET sürümlerinde olduğundan çok daha kısadır. Bombardier'i uç noktasıyla api/diagscenario/taskwait yeniden çalıştırın.

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

.NET 6'da havuzun iş parçacığı sayısını öncekinden daha hızlı artırıp ve yüksek miktarda iş parçacığıyla kararlı hale geldiğini gözlemlemelisiniz. İş parçacığı sayısı tırmanırken ThreadPool açlığı oluşuyor.

ThreadPool aç kalma sorununu çözme

ThreadPool açlığını ortadan kaldırmak için ThreadPool iş parçacıklarının gelen iş öğelerini işlemek için kullanılabilir olması için engelsiz kalması gerekir. Her bir iş parçacığının ne yaptığını belirlemenin birden çok yolu vardır. Sorun yalnızca ara sıra oluşuyorsa, belirli bir süre boyunca uygulama davranışını kaydetmek için dotnet-trace ile bir izleme toplamak en iyisidir. Sorun sürekli oluşuyorsa dotnet-stack aracını kullanabilir veya Visual Studio'da görüntülenebilen dotnet-dump ile dökümü yakalayabilirsiniz. dotnet-stack, iş parçacığı yığınlarını hemen konsolda gösterdiği için daha hızlı olabilir. Ancak Visual Studio dökümü hata ayıklaması, çerçeveleri kaynağa eşleyen daha iyi görselleştirmeler sunar, Yalnızca Benim Kodum çalışma zamanı uygulama çerçevelerini filtreleyebilir ve Paralel Yığınlar özelliği benzer yığınlara sahip çok sayıda iş parçacığını gruplandırmaya yardımcı olabilir. Bu öğreticide dotnet-stack ve dotnet-trace seçenekleri gösterilir. Visual Studio kullanarak iş parçacığı yığınlarını araştırma örneği için Bkz. ThreadPool açlığı tanılama öğreticisi videosu.

dotnet-stack ile devam eden bir sorunu teşhis etme

Web sunucusunu yük altına almak için Bombardier'ı yeniden çalıştırın:

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

Ardından iş parçacığı yığını izlerini görmek için dotnet-stack komutunu çalıştırın.

dotnet-stack report -n DiagnosticScenarios

Çok sayıda yığın içeren ve birçoğu aşağıdaki gibidir şeklinde uzun bir çıktı görmelisiniz.

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()

Bu yığınların altındaki çerçeveler, bu iş parçacıklarının ThreadPool iş parçacıkları olduğunu gösterir:

  System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
  System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()

Üst kısımdaki çerçeveler, DiagnosticScenarioController.TaskWait() işlevinden yapılan çağrıda GetResultCore(bool) iş parçacığının engellendiğini gösterir:

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()

dotnet-trace ile kesintili bir sorunu tanılamak

dotnet-stack yaklaşımı yalnızca her istekte gerçekleşen belirgin ve tutarlı engelleme işlemleri için etkilidir. Bazı senaryolarda engelleme yalnızca birkaç dakikada bir düzensiz olarak gerçekleşir ve dotnet-stack sorunu tanılamak için daha az yararlı olur. Bu durumda dotnet-trace kullanarak belirli bir süre içindeki olayları toplayabilir ve daha sonra analiz edilebilen bir nettrace dosyasına kaydedebilirsiniz.

İş parçacığı havuzu açlığının teşhisine yardımcı olan belirli bir olay vardır: WaitHandleWait olayı, .NET 9'da tanıtılmıştır. Bir iş parçacığı, eşzamanlı-üzerinde-eşzamansız çağrılar (örneğin, Task.Result, Task.Wait, ve Task.GetAwaiter().GetResult()) veya lock, Monitor.Enter, ManualResetEventSlim.Wait, ve SemaphoreSlim.Wait gibi diğer kilitleme işlemleri tarafından engellendiğinde oluşur.

Web sunucusunu yük altına almak için Bombardier'ı yeniden çalıştırın:

bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s

Ardından dotnet-trace komutunu çalıştırarak bekleme olaylarını toplayın:

dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30

Bu, olayları içeren adlı DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace bir dosya oluşturmalıdır. Bu nettrace iki farklı araç kullanılarak analiz edilebilir:

Aşağıdaki bölümlerde nettrace dosyasını okumak için her aracın nasıl kullanılacağı gösterilmektedir.

Perfview ile bir nettrace analizi yapmak

  1. PerfView'u indirin ve çalıştırın.

  2. Nettrace dosyasını çift tıklayarak açın.

    PerfView'da nettrace açma işleminin ekran görüntüsü

  3. Gelişmiş Grup>Herhangi Bir Yığın'a çift tıklayın. Yeni bir pencere açılır.

    PerfView'daki tüm yığınlar görünümünün ekran görüntüsü.

  4. "Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start etkinlik çizgisine çift tıklayın."

    Şimdi WaitHandleWait olaylarının yayıldığı yığın izlerini görmelisiniz. Bunlar "WaitSource" tarafından ayrılmıştır. Şu anda iki kaynak vardır: MonitorWaitMonitor.Wait aracılığıyla yayılan olaylar ve Unknown diğerleri için.

    PerfView'da bekleme olayları için herhangi bir yığın görünümünün ekran görüntüsü.

  5. MonitorWait ile başlayın, çünkü olayların %64,8'i% temsil ediyor. Bu olayın yayılmasından sorumlu yığın izlemelerini genişletmek için onay kutularını işaretleyebilirsiniz.

    PerfView'da bekleme olayları için genişletilmiş tüm yığınlar görünümünün ekran görüntüsü.

    Bu yığın izlemesi şu şekilde okunabilir: Task<T>.Result bir WaitSource MonitorWait ile WaitHandleWait olayı yaydı (Task<T>.Result bekleme gerçekleştirmek için Monitor.Wait kullanır). DiagScenarioController.TaskWait tarafından çağrıldı, bazı ASP.NET kodu tarafından çağrılan bir lambda tarafından çağrılmıştı.

.NET Olay Görüntüleyicisi ile nettrace çözümleme

  1. verdie-g.github.io/dotnet-events-viewer gidin.

  2. Nettrace dosyasını sürükleyip bırakın.

    .NET Olay Görüntüleyicisi'nde bir nettrace'in açılmasının ekran görüntüsü.

  3. Olay Ağacı sayfasına gidin, "WaitHandleWaitStart" olayını seçin ve ardından Sorguyu çalıştır'ı seçin.

    .NET Olay Görüntüleyicisi'nde bir olay sorgusunun ekran görüntüsü.

  4. WaitHandleWait olaylarının yayıldığı yığın izlemelerini görmeniz gerekir. Bu olayın yayılmasından sorumlu yığın izlemelerini genişletmek için oklara tıklayın.

    .NET Olay Görüntüleyicisi'ndeki ağaç görünümünün ekran görüntüsü.

    Bu yığın izi şu şekilde okunabilir: ManualResetEventSlim.Wait bir WaitHandleWait olayı yaydı. Task.SpinThenBlockWait tarafından çağrıldı, Task.InternalWaitCore tarafından çağrıldı, Task<T>.Result tarafından çağrıldı, DiagScenario.TaskWait tarafından çağrıldı, bir lambda tarafından çağrıldı, bazı ASP.NET kodu tarafından çağrıldı.

Gerçek dünya senaryolarında, iş parçacığı havuzunun dışında kalan iş parçacıklarından yayılan çok sayıda bekleme olayıyla karşılaşabilirsiniz. Burada bir iş parçacığı havuzunun aç kalmasıyla ilgili araştırma yaptığınız için, iş parçacığı havuzunun dışındaki ayrılmış iş parçacığında tüm beklemeler ilgili değildir. Bir yığın izlemesinin bir iş parçacığı havuzu iş parçacığından olup olmadığını, ilk yöntemlere bakarak anlayabilirsiniz; bu yöntemlerde iş parçacığı havuzundan bahsedilmesi gerekebilir (örneğin, WorkerThread.WorkerThreadStart veya ThreadPoolWorkQueue).

İş parçacığı havuzu iş parçacığı yığın izlemesinin üst kısmı.

Kod düzeltmesi

Artık zaman uyumsuz bir API'nin kullanılmadan çağrıldığını görmek için, örnek uygulamanın await dosyasındaki bu denetleyicinin koduna gidebilirsiniz. Bu, iş parçacıklarını engellediği bilinen ve ThreadPool kaynaklarının tükenmesinin en yaygın nedeni olan senkron-asenkron kod desenidir.

public ActionResult<string> TaskWait()
{
    // ...
    Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
    return "success:taskwait";
}

Bu durumda kod, uç noktada gösterildiği TaskAsyncWait() gibi async/await kullanmak için kolayca değiştirilebilir. Await kullanılması, veritabanı sorgusu devam ederken geçerli iş parçacığının diğer iş öğelerine hizmet vermesine olanak tanır. Veritabanı araması tamamlandığında ThreadPool iş parçacığı yürütmeyi sürdürür. Bu şekilde her istek sırasında kodda hiçbir iş parçacığı engellenmez.

public async Task<ActionResult<string>> TaskAsyncWait()
{
    // ...
    Customer c = await PretendQueryCustomerFromDbAsync("Dana");
    return "success:taskasyncwait";
}

Bombardier yükü api/diagscenario/taskasyncwait uç noktasına göndermek için çalıştırıldığında, ThreadPool iş parçacığı sayısının çok daha düşük kaldığını ve asenkron/await yaklaşımı kullanılırken ortalama gecikme süresinin 500 ms civarında kaldığını gösterir.

>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