Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Artikel ini berlaku untuk: ✔️ .NET 9.0 dan versi yang lebih baru
Dalam tutorial ini, Anda akan mempelajari cara men-debug skenario kelaparan ThreadPool. Kehabisan sumber daya ThreadPool terjadi ketika pool tidak memiliki utas yang tersedia untuk memproses item kerja baru, dan hal ini sering menyebabkan aplikasi merespons dengan lambat. Dengan menggunakan contoh aplikasi web ASP.NET Core, Anda dapat menyebabkan kekurangan ThreadPool dengan sengaja dan mempelajari cara mendiagnosisnya.
Dalam tutorial ini, Anda akan:
- Menyelidiki aplikasi yang merespons permintaan secara perlahan
- Gunakan alat dotnet-counters untuk mengidentifikasi bahwa kelaparan ThreadPool kemungkinan besar terjadi.
- Gunakan alat dotnet-stack dan dotnet-trace untuk menentukan pekerjaan apa yang membuat utas ThreadPool tetap bekerja.
Prasyarat
Tutorial ini menggunakan:
- .NET 9 SDK untuk membangun dan menjalankan aplikasi sampel
- Contoh aplikasi web untuk menunjukkan perilaku kelaparan ThreadPool
- Bombardier untuk menghasilkan beban untuk aplikasi web sampel
- dotnet-counters untuk mengamati penghitung kinerja
- dotnet-stack untuk memeriksa tumpukan utas
- dotnet-trace untuk mengumpulkan peristiwa tunggu
- Opsional: PerfView untuk menganalisis peristiwa tunggu
Menjalankan aplikasi sampel
Unduh kode untuk aplikasi sampel dan jalankan menggunakan .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
Jika Anda menggunakan browser web dan mengirim permintaan ke https://localhost:5001/api/diagscenario/taskwait, Anda akan melihat respons success:taskwait yang dikembalikan setelah sekitar 500 ms. Ini menunjukkan bahwa server web melayani lalu lintas seperti yang diharapkan.
Mengamati performa lambat
Server web demo memiliki beberapa titik akhir yang meniru melakukan permintaan database lalu mengembalikan respons kepada pengguna. Masing-masing titik akhir ini memiliki penundaan sekitar 500 ms saat melayani permintaan satu per satu tetapi performanya jauh lebih buruk ketika server web mengalami beberapa beban. Unduh alat pengujian beban Bombardier dan amati perbedaan latensi saat 125 permintaan bersamaan dikirim ke setiap titik akhir.
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
Titik akhir kedua ini menggunakan pola kode yang berkinerja lebih buruk:
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
Kedua pos akhir ini menunjukkan latensi yang jauh lebih tinggi dari rata-rata 500 ms ketika beban tinggi (3,48 detik dan 15,42 detik masing-masing untuk setiap pos akhir). Jika Anda menjalankan contoh ini pada versi .NET Core yang lebih lama, Anda mungkin melihat kedua contoh berkinerja sama buruknya. .NET 6 telah memperbarui heuristik ThreadPool yang mengurangi dampak performa pola pengodean buruk yang digunakan dalam contoh pertama.
Mendeteksi kekurangan sumber daya ThreadPool
Jika Anda mengamati perilaku di atas pada layanan dunia nyata, Anda akan tahu itu merespons dengan lambat di bawah beban tetapi Anda tidak akan tahu penyebabnya. dotnet-counters adalah alat yang dapat menampilkan penghitung kinerja secara real-time. Penghitung ini dapat memberikan petunjuk tentang masalah tertentu dan sering kali mudah didapatkan. Di lingkungan produksi, Anda mungkin memiliki penghitung serupa yang disediakan oleh alat pemantauan jarak jauh dan dasbor web. Instal penghitung dotnet dan mulai pantau layanan 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
Jika aplikasi Anda menjalankan versi .NET yang lebih lama dari .NET 9, antarmuka pengguna output penghitung dotnet akan terlihat sedikit berbeda; lihat penghitung dotnet untuk detailnya.
Penghitung sebelumnya adalah contoh saat server web tidak melayani permintaan apa pun. Jalankan Bombardier lagi selama 2 menit dengan titik akhir api/diagscenario/tasksleepwait dan beban berkelanjutan sehingga ada banyak waktu untuk mengamati apa yang terjadi pada penghitung performa.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
Terjadi kelangkaan utas ThreadPool ketika tidak ada utas yang tersedia untuk menangani tugas yang ada dalam antrean dan runtime merespons dengan meningkatkan jumlah utas ThreadPool. Nilai dotnet.thread_pool.thread.count meningkat dengan cepat menjadi 2-3x jumlah inti prosesor pada komputer Anda, dan kemudian utas lebih lanjut ditambahkan 1-2 per detik sampai stabil di suatu tempat di atas 125. Sinyal utama kelaparan ThreadPool yang saat ini menjadi hambatan performa adalah peningkatan utas ThreadPool yang lambat dan stabil serta penggunaan CPU yang jauh di bawah 100%. Peningkatan jumlah utas akan berlanjut sampai kumpulan mencapai jumlah maksimum utas, utas yang cukup telah dibuat untuk memenuhi semua item kerja yang masuk, atau saat CPU telah mencapai titik jenuh. Sering, tetapi tidak selalu, kelaparan ThreadPool juga akan menunjukkan nilai besar untuk dotnet.thread_pool.queue.length dan nilai rendah untuk dotnet.thread_pool.work_item.count, yang berarti ada banyak pekerjaan yang tertunda dan hanya sedikit yang diselesaikan. Berikut adalah contoh dari penghitung angka saat jumlah thread masih meningkat:
[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
Setelah jumlah utas ThreadPool stabil, kumpulan tidak lagi kelaparan. Tetapi jika stabil pada nilai tinggi (lebih dari sekitar tiga kali jumlah inti prosesor), yang biasanya menunjukkan kode aplikasi memblokir beberapa utas ThreadPool dan ThreadPool mengimbangi dengan menjalankan lebih banyak utas. Menjaga stabilitas pada jumlah utas yang tinggi tidak akan selalu berdampak besar pada latensi permintaan, tetapi jika beban bervariasi secara dramatis dari waktu ke waktu atau aplikasi akan dihidupkan ulang secara berkala, maka setiap kali, ThreadPool cenderung memasuki periode kelaparan di mana ia perlahan meningkatkan jumlah utas dan menghasilkan latensi permintaan yang buruk. Setiap utas juga mengonsumsi memori, sehingga mengurangi jumlah total utas yang diperlukan memberikan manfaat lain.
Mulai dari .NET 6, heuristik ThreadPool dimodifikasi untuk meningkatkan jumlah thread ThreadPool dengan jauh lebih cepat sebagai respons terhadap API Task yang memblokir. Kekurangan sumber daya pada ThreadPool masih bisa terjadi dengan API ini, namun durasinya lebih singkat jika dibandingkan dengan versi .NET yang lebih lama karena runtime merespons lebih cepat. Jalankan Bombardier lagi pada alamat tujuan api/diagscenario/taskwait.
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Pada .NET 6, Anda dapat mengamati bahwa kumpulan utas meningkatkan jumlahnya lebih cepat dari sebelumnya dan kemudian stabil pada jumlah utas yang tinggi. Starvasi ThreadPool terjadi ketika jumlah utas meningkat.
Mengatasi kelaparan ThreadPool
Untuk mengatasi ketidaktersediaan ThreadPool, utas ThreadPool harus tetap tidak diblokir agar dapat tersedia untuk menangani item kerja yang masuk. Ada beberapa cara untuk menentukan apa yang dilakukan setiap utas. Jika masalah hanya terjadi sesekali, maka mengumpulkan jejak dengan dotnet-trace adalah yang terbaik untuk merekam perilaku aplikasi selama jangka waktu tertentu. Jika masalah terjadi terus-menerus, maka Anda dapat menggunakan alat dotnet-stack atau mengambil cadangan dengan dotnet-dump yang dapat dilihat di Visual Studio. dotnet-stack bisa lebih cepat karena menampilkan tumpukan utas langsung di konsol. Tetapi analisis berkas dump di Visual Studio menawarkan visualisasi yang lebih baik dengan memetakan frame ke sumber, Just My Code dapat menyaring frame implementasi runtime, dan fitur Parallel Stacks dapat membantu mengelompokkan sejumlah besar utas dengan tumpukan yang serupa. Tutorial ini menunjukkan opsi dotnet-stack dan dotnet-trace. Untuk contoh meneliti tumpukan thread menggunakan Visual Studio, lihat video tutorial mendiagnosis kelaparan ThreadPool.
Mendiagnosis masalah berkelanjutan dengan dotnet-stack
Jalankan Bombardier lagi untuk menempatkan server web di bawah beban:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Kemudian jalankan dotnet-stack untuk melihat jejak tumpukan utas:
dotnet-stack report -n DiagnosticScenarios
Anda akan melihat output panjang yang berisi sejumlah besar tumpukan, banyak di antaranya terlihat seperti ini:
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()
Bingkai di bagian bawah tumpukan ini menunjukkan bahwa utas ini adalah utas ThreadPool:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
Dan bingkai di dekat bagian atas mengungkapkan bahwa utas diblokir pada panggilan ke GetResultCore(bool) dari fungsi 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()
Mendiagnosis masalah yang terjadi secara intermittens menggunakan dotnet-trace
Pendekatan dotnet-stack hanya efektif untuk operasi pemblokiran yang jelas dan konsisten yang terjadi di setiap permintaan. Dalam beberapa skenario, pemblokiran terjadi secara sporatik hanya setiap beberapa menit, membuat dotnet-stack kurang berguna untuk mendiagnosis masalah. Dalam hal ini, Anda dapat menggunakan dotnet-trace untuk mengumpulkan peristiwa selama jangka waktu tertentu dan menyimpannya dalam file nettrace yang dapat dianalisis nanti.
Ada satu peristiwa tertentu yang membantu mendiagnosis kelaparan kumpulan utas: peristiwa WaitHandleWait, yang diperkenalkan di .NET 9. Ini dipancarkan ketika utas diblokir oleh operasi seperti panggilan sync-over-async (misalnya, Task.Result, Task.Wait, dan Task.GetAwaiter().GetResult()) atau oleh operasi penguncian lainnya seperti lock, Monitor.Enter, ManualResetEventSlim.Wait, dan SemaphoreSlim.Wait.
Jalankan Bombardier lagi untuk menempatkan server web di bawah beban:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
Kemudian jalankan dotnet-trace untuk mengumpulkan peristiwa tunggu:
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
Itu harus menghasilkan file bernama DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace yang berisi peristiwa. Nettrace ini dapat dianalisis menggunakan dua alat yang berbeda:
- PerfView: Alat analisis performa yang dikembangkan oleh Microsoft untuk Windows saja.
- .NET Events Viewer: Alat analisis nettrace web Blazor yang dikembangkan oleh komunitas.
Bagian berikut menunjukkan cara menggunakan setiap alat untuk membaca file nettrace.
Menganalisis nettrace dengan Perfview
Unduh PerfView dan jalankan.
Buka file nettrace dengan mengklik dua kali.
Klik dua kali pada Grup Tingkat Lanjut>Tumpukan Mana Saja. Jendela baru terbuka.
Klik dua kali pada baris "Event Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start".
Sekarang Anda akan melihat jejak tumpukan tempat peristiwa WaitHandleWait dipancarkan. Mereka dibagi oleh "WaitSource". Saat ini ada dua sumber:
MonitorWaituntuk peristiwa yang dipancarkan melalui Monitor.Wait, danUnknownuntuk semua sumber lainnya.Mulai dari MonitorWait karena itu mewakili 64,8% dari peristiwa%. Anda dapat memeriksa kotak centang untuk mengembangkan jejak tumpukan yang bertanggung jawab untuk menghasilkan peristiwa ini.
Jejak tumpukan ini dapat dibaca sebagai:
Task<T>.Resultmenghasilkan peristiwa WaitHandleWait dengan WaitSource MonitorWait (Task<T>.ResultmenggunakanMonitor.Waituntuk melakukan penantian). Itu dipanggil olehDiagScenarioController.TaskWait, yang dipanggil oleh lambda tertentu, yang dipanggil oleh kode ASP.NET tertentu.
Menganalisis nettrace dengan .NET Events Viewer
Seret dan letakkan file nettrace.
Buka halaman Pohon Peristiwa , pilih peristiwa "WaitHandleWaitStart", lalu pilih Jalankan kueri.
Anda akan melihat jejak tumpukan tempat peristiwa WaitHandleWait dipancarkan. Klik panah-panah untuk memperluas jejak tumpukan yang bertanggung jawab untuk menghasilkan kejadian ini.
Jejak tumpukan ini dapat dibaca sebagai:
ManualResetEventSlim.Waitmenghasilkan peristiwa WaitHandleWait. Itu dipanggil olehTask.SpinThenBlockWait, yang dipanggil olehTask.InternalWaitCore, yang dipanggil olehTask<T>.Result, yang dipanggil olehDiagScenario.TaskWait, yang dipanggil oleh suatu lambda, yang dipanggil oleh suatu kode ASP.NET.
Dalam skenario dunia nyata, Anda mungkin menemukan banyak peristiwa tunggu yang dipancarkan dari utas di luar kumpulan utas. Di sini, Anda sedang menyelidiki kelaparan kumpulan utas, sehingga semua penantian pada utas khusus di luar kumpulan utas menjadi tidak relevan. Anda dapat mengetahui apakah jejak tumpukan berasal dari utas di kumpulan utas dengan melihat metode-metode pertama, yang harus memuat penyebutan kumpulan utas (misalnya, WorkerThread.WorkerThreadStart atau ThreadPoolWorkQueue).
Perbaikan kode
Sekarang Anda dapat menavigasi ke kode untuk pengontrol ini dalam file Controllers/DiagnosticScenarios.cs aplikasi sampel untuk melihat bahwa ia memanggil API asinkron tanpa menggunakan await. Ini adalah pola kode sync-over-async, yang diketahui memblokir utas dan merupakan penyebab paling umum dari kelaparan ThreadPool.
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
Dalam kasus ini, kode dapat dengan mudah diubah untuk menggunakan async/await sebagai gantinya, seperti yang ditunjukkan di TaskAsyncWait() titik akhir. Menggunakan menunggu memungkinkan proses saat ini untuk melayani tugas lain sementara kueri database sedang berlangsung. Ketika pencarian database selesai, utas ThreadPool akan melanjutkan eksekusi. Dengan demikian, tidak ada utas yang terblokir dalam kode selama setiap permintaan.
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
Menjalankan Bombadier untuk mengirim beban ke api/diagscenario/taskasyncwait titik akhir menunjukkan bahwa jumlah utas ThreadPool tetap jauh lebih rendah dan latensi rata-rata tetap dekat 500ms saat menggunakan pendekatan asinkron/tunggu:
>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