【问题标题】:Does Using Async Await Avoid Thread Exhaustion?使用 Async Await 是否可以避免线程耗尽?
【发布时间】:2023-12-30 20:01:01
【问题描述】:

我们正在对.NET Core API 端点上的以下性能问题进行故障排除:

  1. 在较小的负载下,端点始终以小于500MS 的速度返回。
  2. 当我们从 3 个浏览器访问端点时,每秒一个请求,它逐渐变慢(在添加第三个浏览器进行调用的一分钟内,响应时间下降到 50,000MS 或更糟.
  3. 每个额外的浏览器添加 API 使用的线程,例如40 个线程基础,第二个浏览器访问端点导致 52 个线程,第三个峰值达到 70 个,依此类推。
  4. 当加载一个端点时,整个 API 会缓慢返回(所有端点)。这是我与第 3 点一起思考“线程耗尽”的主要原因。

目前的代码如下所示:

    public IActionResult GetPresentationByEvent(int eventid)
    {
      return Authorized(authDto =>
      {
        var eventList = _eventService.GetPresentationByEvent(eventid);
        return Ok(eventList)
      })
    }

我的理论是return Authorized(authDto => 持有一个线程直到它返回,导致线程耗尽。

    public async Task<IActionResult> GetPresentationByEvent(int eventid)
    {
      return Authorized(async authDto =>
      {
        Task<List<whatever>> eventList = _eventService.GetPresentationByEvent(eventid);
        return Ok(eventList)
      }
    }

Authorized 是第三方库的一部分,所以我不能轻易测试。想知道这是否是一个可能的问题/解决方案。

【问题讨论】:

  • 这只有在调用一直是异步的情况下才有效,例如直到数据库/文件访问。见blog.stephencleary.com/2013/11/there-is-no-thread.html
  • “第三次飙升至 70”非常奇怪。那些线程在做什么?预热后你应该有一个基本的线程数,之后的每个并发请求应该只增加一个线程。
  • 我现在正在调查。我更改了调用以返回一个新列表,而无需去 db / 执行任何身份验证。每个浏览器也建立了一个 SignalR 连接,所以也许我正在那里启动线程并且端点变慢只是一个副作用。现在将进行测试。 Ty 的回复,他们真的在帮助我思考这个问题。
  • @StephenCleary 让我纠正一下自己 - 每个浏览器都在运行一个负载测试功能,它每秒向 API 发出一个请求。所以,从这个角度来看,我认为线程的数量是有意义的。附言- 它不是 signalR,在没有到达此端点的情况下建立了 SignalR 连接,并且端点/整个 API 响应良好。现在更加迷失了,因为让端点返回一个空列表仍然无法处理每秒 3 个请求(来自 3 个浏览器)。

标签: c# multithreading asp.net-core-2.1 non-exhaustive-patterns


【解决方案1】:

是的,异步等待可以减少线程耗尽。简而言之,当您生成的任务超出 ThreadPool 可以处理的数量时,就会出现线程耗尽。

您可以在这里查看一些细微的细节:Thread starvation and queuing

您唯一需要记住的是,您永远不应该在任务中阻塞。这意味着使用 async await 调用异步代码(并且永远不要在未完成的任务上使用 .Wait 或 .Result)。

如果你使用了一些没有使用异步等待模式的阻塞代码,你必须在专用线程(而不是任务线程队列)上生成它。

【讨论】:

    【解决方案2】:

    我的理论是 return Authorized(authDto => 持有一个线程直到它返回,导致线程耗尽。

    是的。您可以通过查看方法的返回值轻松判断方法是否同步。 IActionResult 不是等待类型,所以这个方法会同步运行。

    Authorized 是第三方库的一部分,所以我不能轻易测试。想知道这是否是一个可能的问题/解决方案。

    可能。这完全取决于Authorized 是否可以处理异步委托。如果可以,那么这样的事情会起作用:

    public async Task<IActionResult> GetPresentationByEvent(int eventid)
    {
      return Authorized(async authDto =>
      {
        Task<List<whatever>> eventList = _eventService.GetPresentationByEventAsync(eventid);
        return Ok(await eventList);
      });
    }
    

    注意:

    1. 任务在传递给 Ok 或其他帮助者之前应该是 awaited。
    2. 这引入了GetPresentationByEventAsync,假设您的数据访问代码可以异步进行。

    由于使GetPresentationByEvent 异步可能需要一些工作,因此值得在尝试此操作之前调查Authorized 是否可以接受异步委托。

    使用 Async Await 是否可以避免线程耗尽?

    是的,不是的。异步代码(包括async/await)确实使用更少的线程,因为它避免了阻塞线程。但是,仍然有一个限制。由于异步代码需要一个空闲线程来完成,线程耗尽仍然是可能的。使用异步代码,通常可以在遇到线程耗尽等可伸缩性问题之前实现一个或两个数量级的可伸缩性。

    有关async ASP.NET 的更多概念信息,请参阅this MSDN article

    【讨论】:

    • 感谢您的回复。看起来这是问题的一部分,但在我的问题范围之外还有其他事情需要深入研究。接受第一个答案,因为他是第一个,但非常感谢您的帮助。