针对 ThreadPool 资源不足问题进行调试

本文适用于:✔️ .NET 9.0 及更高版本

本教程介绍如何调试 ThreadPool 饥饿方案。 当 ThreadPool 发生饥饿现象,即线程池没有可用线程来处理新的工作项时,往往会导致应用程序响应缓慢。 使用提供的示例 ASP.NET Core Web 应用,可以有意导致 ThreadPool 耗尽,并了解如何诊断它。

在本教程中,你将:

  • 调查对请求响应缓慢的应用程序
  • 使用 dotnet-counters 工具检查是否可能存在线程池资源耗尽的问题。
  • 使用 dotnet-stack 和 dotnet-trace 工具确定哪些工作使 ThreadPool 线程保持忙碌

先决条件

本教程使用:

运行示例应用

下载 示例应用 的代码,并使用 .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.ResultTask.WaitTask.GetAwaiter().GetResult())或其他锁定操作(例如 lockMonitor.EnterManualResetEventSlim.WaitSemaphoreSlim.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:

以下部分演示如何使用每个工具读取 nettrace 文件。

使用 Perfview 分析 nettrace

  1. 下载 PerfView 并运行它。

  2. 双击 nettrace 文件,打开 nettrace 文件。

    在 PerfView 中打开 nettrace 的屏幕截图

  3. 双击 高级组>任意堆栈。 新 窗口将打开。

    PerfView 中任何堆栈视图的屏幕截图。

  4. 双击“事件Microsoft-Windows-DotNETRuntime/WaitHandleWait/Start”行。

    现在您应该可以看到发出 WaitHandleWait 事件的堆栈跟踪。 它们由“WaitSource”拆分。 目前有两个源: MonitorWait 对于通过 Monitor.Wait 发出的事件,以及 Unknown 所有其他源。

    PerfView 中等待事件的任何堆栈视图的屏幕截图。

  5. 从 MonitorWait 开始,因为它代表事件的 64.8%。 可以选中复选框以展开负责发出此事件的堆栈跟踪。

    PerfView 中等待事件的展开任意堆栈视图的屏幕截图。

    此堆栈跟踪可以解释为:Task<T>.Result 发出一个 WaitHandleWait 事件,其中 WaitSource 是 MonitorWait(Task<T>.Result 利用 Monitor.Wait 来执行等待)。 它是由DiagScenarioController.TaskWait调用的,而调用它的是某个 lambda 函数,后者又是由某些 ASP.NET 代码调用的

使用 .NET 事件查看器分析 nettrace

  1. 转到 verdie-g.github.io/dotnet-events-viewer

  2. 将 nettrace 文件拖动并放置。

    .NET 事件查看器中打开 nettrace 的屏幕截图。

  3. 转到 “事件树 ”页,选择事件“WaitHandleWaitStart”,然后选择“ 运行查询”。

    .NET 事件查看器中事件查询的屏幕截图。

  4. 您应该能看到发出 WaitHandleWait 事件的堆栈跟踪。 点击箭头以展开导致此事件的堆栈跟踪。

    .NET 事件查看器中树视图的屏幕截图。

    此堆栈跟踪可以读取为: ManualResetEventSlim.Wait 发出 WaitHandleWait 事件。 它被 Task.SpinThenBlockWait 调用,Task.SpinThenBlockWaitTask.InternalWaitCore 调用,Task.InternalWaitCoreTask<T>.Result 调用,Task<T>.ResultDiagScenario.TaskWait 调用,DiagScenario.TaskWait 被一些 lambda 调用,而 lambda 被一些 ASP.NET 代码调用。

在实际场景中,你可能会发现从线程池外部的线程发出的大量等待事件。 在这里,你正在研究 线程池 的资源不足问题,因此任何在线程池之外的专用线程上发生的等待都不相关。 可以通过查看第一种方法来判断堆栈跟踪是否来自线程池线程,该方法应包含线程池的提及(例如, WorkerThread.WorkerThreadStartThreadPoolWorkQueue)。

线程池线程堆栈跟踪的顶部。

代码修复

现在,您可以导航到示例应用的 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