本文适用于:✔️ .NET 9.0 及更高版本
本教程介绍如何调试 ThreadPool 饥饿方案。 当 ThreadPool 发生饥饿现象,即线程池没有可用线程来处理新的工作项时,往往会导致应用程序响应缓慢。 使用提供的示例 ASP.NET Core Web 应用,可以有意导致 ThreadPool 耗尽,并了解如何诊断它。
在本教程中,你将:
- 调查对请求响应缓慢的应用程序
- 使用 dotnet-counters 工具检查是否可能存在线程池资源耗尽的问题。
- 使用 dotnet-stack 和 dotnet-trace 工具确定哪些工作使 ThreadPool 线程保持忙碌
先决条件
本教程使用:
- 用于生成和运行示例应用的 .NET 9 SDK
- 示例 Web 应用用于演示 ThreadPool 饥饿行为
- 庞巴迪 为示例 Web 应用生成负载
- 用于观察性能计数器的 dotnet-counters
- dotnet-stack 用于检查线程的堆栈
- 用于收集等待事件的 dotnet-trace
- 可选: PerfView 分析等待事件
运行示例应用
下载 示例应用 的代码,并使用 .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
如果使用 Web 浏览器并向其发送请求 https://localhost:5001/api/diagscenario/taskwait
,则应在大约 500 毫秒后看到返回的响应 success:taskwait
。 这表明 Web 服务器按预期提供流量。
观察性能表现缓慢
演示 Web 服务器有多个终结点,这些终结点模拟执行数据库请求,然后向用户返回响应。 每个终结点在一次处理请求时延迟大约为 500 毫秒,但当 Web 服务器受到某些负载的影响时,性能会更糟。 下载 庞巴迪 负载测试工具,并观察向每个终结点发送 125 个并发请求时的延迟差异。
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
第二个终结点使用性能更差的代码模式:
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
这两个终结点在负载较高(3.48 秒和 15.42 秒)时的平均延迟明显超过 500 毫秒的平均延迟。 如果在较旧版本的 .NET Core 上运行此示例,则可能会看到这两个示例执行得同样糟糕。 .NET 6 更新了 ThreadPool 启发式方法,可降低第一个示例中使用的错误编码模式的性能影响。
检测 ThreadPool 资源耗尽
如果在实际服务上观察到上述行为,你会知道它在负载下缓慢响应,但你不知道原因。 dotnet-counters 是一种可显示实时性能计数器的工具。 这些计数器可以提供有关某些问题的线索,而且往往很容易得到。 在生产环境中,你可能具有远程监视工具和 Web 仪表板提供的类似计数器。 安装 dotnet-counters 并开始监视 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
前面的计数器是一个示例,而 Web 服务器未提供任何请求。 再次使用 api/diagscenario/tasksleepwait
终结点运行庞巴迪,负载持续2分钟,以便有足够的时间观测性能计数器的变化。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/tasksleepwait -d 120s
当发生线程池饥饿时,就意味着没有可用的线程来处理排队的工作项,运行时通过增加线程池中的线程数量来应对。 该值 dotnet.thread_pool.thread.count
迅速增加到计算机上的处理器核心数的 2-3 倍,然后进一步的线程将每秒添加 1-2,直到稳定在 125 以上的某个位置。 ThreadPool 耗尽作为性能瓶颈的关键信号表现为 ThreadPool 线程的缓慢和稳定增长,以及 CPU 使用率远低于 100%。 线程计数将继续增加,直到发生以下情况之一:池达到最大线程数,已创建足够的线程以满足所有传入的工作项,或者 CPU 已达到饱和状态。 线程池饥饿通常会显示 dotnet.thread_pool.queue.length
的值较大和 dotnet.thread_pool.work_item.count
的值较低,这意味着有大量待处理的工作而完成的工作很少。 下面是线程计数仍在上升时的计数器示例:
[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 线程,而 ThreadPool 通过运行更多线程来进行补偿。 在高线程计数下保持稳定运行不一定对请求延迟产生重大影响,但如果负载随时间变化很大,或者应用将定期重启,则每次 ThreadPool 都可能会进入一段饥饿期,其中线程缓慢增加,并且提供请求延迟不佳。 每个线程也会消耗内存,因此减少所需的线程总数可提供另一个好处。
从 .NET 6 开始,ThreadPool 启发策略已被修改,以更快地增加 ThreadPool 线程数,以响应某些阻塞的任务 API。 线程池的饥饿仍可通过这些 API 发生,但持续时间比旧版 .NET 版本要短得多,因为运行时响应速度更快。 使用 api/diagscenario/taskwait
终结点再次运行庞巴迪:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
在 .NET 6 上,您应该观察到池中的线程计数比以前增加得更快,然后稳定在一个较高的线程数量。 线程池正在发生饥饿,同时线程计数正在攀升。
解决 ThreadPool 饥饿问题
若要消除 ThreadPool 饥饿,ThreadPool 线程需要保持未阻止状态,以便它们可用于处理传入的工作项。 有多种方法可以确定每个线程正在做的工作。 如果问题仅偶尔发生,那么使用 dotnet-trace 收集跟踪记录,在一段时间内监测应用程序行为是最佳选择。 如果问题不断发生,则可以使用 dotnet-stack 工具,或使用 dotnet-dump 捕获转储,并在 Visual Studio 中查看。 dotnet-stack 可能更快,因为它会立即在控制台上显示线程堆栈。 但是 Visual Studio 转储调试提供了更好的可视化效果,可将帧映射到源,“仅我的代码”可以筛选掉运行时实现帧,并行堆栈功能可以帮助将大量线程与类似的堆栈组合在一起。 本教程介绍 dotnet-stack 和 dotnet-trace 选项。 有关使用 Visual Studio 调查线程堆栈的示例,请参阅 诊断 ThreadPool 饥饿教程视频。
诊断 dotnet-stack 的持续问题
再次运行 Bombardier,对 Web 服务器进行负载测试:
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
然后运行 dotnet-stack 以查看线程堆栈跟踪:
dotnet-stack report -n DiagnosticScenarios
应会看到包含大量堆栈的长输出,其中许多如下所示:
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()
这些堆栈底部的帧指示这些线程是 ThreadPool 线程:
System.Private.CoreLib.il!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.il!System.Threading.PortableThreadPool+WorkerThread.WorkerThreadStart()
接近顶部的帧显示线程在调用 DiagnosticScenarioController.TaskWait() 函数的 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()
诊断 dotnet-trace 的间歇性问题
dotnet-stack 方法仅对每个请求中发生的明显、一致的阻塞操作有效。 在某些情况下,仅每隔几分钟就发生一次阻塞,从而使 dotnet-stack 对诊断问题不太有用。 在这种情况下,可以使用 dotnet-trace 在一段时间内收集事件,并将其保存在稍后可以分析的 nettrace 文件中。
有一个特定事件有助于诊断线程池饥饿:在 .NET 9 中引入的 WaitHandleWait 事件。 当线程因同步覆盖异步调用(例如 Task.Result
、Task.Wait
、Task.GetAwaiter().GetResult()
)或其他锁定操作(例如 lock
、Monitor.Enter
、ManualResetEventSlim.Wait
、SemaphoreSlim.Wait
)而被阻塞时,会发出信号。
再次运行 Bombardier,加重互联网服务器的负载。
bombardier-windows-amd64.exe https://localhost:5001/api/diagscenario/taskwait -d 120s
然后运行 dotnet-trace 以收集等待事件:
dotnet trace collect -n DiagnosticScenarios --clrevents waithandle --clreventlevel verbose --duration 00:00:30
这应生成一个名为 DiagnosticScenarios.exe_yyyyddMM_hhmmss.nettrace
包含事件的文件。 可以使用两种不同的工具分析此 nettrace:
- PerfView:仅限 Windows Microsoft开发的性能分析工具。
- .NET 事件查看器:社区开发的 nettrace 分析 Blazor Web 工具。
以下部分演示如何使用每个工具读取 nettrace 文件。
使用 Perfview 分析 nettrace
下载 PerfView 并运行它。
双击 nettrace 文件,打开 nettrace 文件。
双击 高级组>任意堆栈。 新 窗口将打开。
双击“事件Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start”行。
现在您应该可以看到发出 WaitHandleWait 事件的堆栈跟踪。 它们由“WaitSource”拆分。 目前有两个源:
MonitorWait
对于通过 Monitor.Wait 发出的事件,以及Unknown
所有其他源。从 MonitorWait 开始,因为它代表事件的 64.8%。 可以选中复选框以展开负责发出此事件的堆栈跟踪。
此堆栈跟踪可以解释为:
Task<T>.Result
发出一个 WaitHandleWait 事件,其中 WaitSource 是 MonitorWait(Task<T>.Result
利用Monitor.Wait
来执行等待)。 它是由DiagScenarioController.TaskWait
调用的,而调用它的是某个 lambda 函数,后者又是由某些 ASP.NET 代码调用的
使用 .NET 事件查看器分析 nettrace
将 nettrace 文件拖动并放置。
转到 “事件树 ”页,选择事件“WaitHandleWaitStart”,然后选择“ 运行查询”。
您应该能看到发出 WaitHandleWait 事件的堆栈跟踪。 点击箭头以展开导致此事件的堆栈跟踪。
此堆栈跟踪可以读取为:
ManualResetEventSlim.Wait
发出 WaitHandleWait 事件。 它被Task.SpinThenBlockWait
调用,Task.SpinThenBlockWait
被Task.InternalWaitCore
调用,Task.InternalWaitCore
被Task<T>.Result
调用,Task<T>.Result
被DiagScenario.TaskWait
调用,DiagScenario.TaskWait
被一些 lambda 调用,而 lambda 被一些 ASP.NET 代码调用。
在实际场景中,你可能会发现从线程池外部的线程发出的大量等待事件。 在这里,你正在研究 线程池 的资源不足问题,因此任何在线程池之外的专用线程上发生的等待都不相关。 可以通过查看第一种方法来判断堆栈跟踪是否来自线程池线程,该方法应包含线程池的提及(例如, WorkerThread.WorkerThreadStart
或 ThreadPoolWorkQueue
)。
代码修复
现在,您可以导航到示例应用的 Controllers/DiagnosticScenarios.cs 文件中的此控制器代码,查看它在未使用 await
的情况下调用异步 API。 这是 同步-异步混合 代码模式,它已知会阻塞线程,也是导致 ThreadPool 饥饿的最常见原因。
public ActionResult<string> TaskWait()
{
// ...
Customer c = PretendQueryCustomerFromDbAsync("Dana").Result;
return "success:taskwait";
}
在这种情况下,可以轻松更改代码以使用异步/等待,如 TaskAsyncWait()
终结点中所示。 使用 await 允许当前线程在数据库查询正在进行时为其他工作项提供服务。 数据库查找完成后,ThreadPool 线程将恢复执行。 每个请求期间,代码不会阻止任何线程。
public async Task<ActionResult<string>> TaskAsyncWait()
{
// ...
Customer c = await PretendQueryCustomerFromDbAsync("Dana");
return "success:taskasyncwait";
}
运行 Bombadier 以将负载发送到 api/diagscenario/taskasyncwait
终结点时显示 ThreadPool 线程计数会保持低得多,使用 async/await 方法时,平均延迟将保持在 500 毫秒以下:
>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