具有 Task.Run 性能的 ASP.NET Web API 2 异步操作方法

Posted

技术标签:

【中文标题】具有 Task.Run 性能的 ASP.NET Web API 2 异步操作方法【英文标题】:ASP.NET Web API 2 Async action methods with Task.Run performance 【发布时间】:2015-05-23 23:02:47 【问题描述】:

我正在尝试对几个 ASP.NET Web API 2.0 端点进行基准测试(使用 Apache bench)。其中一个是同步的,一个是异步的。

        [Route("user/userId/feeds")]
        [HttpGet]
        public IEnumerable<NewsFeedItem> GetNewsFeedItemsForUser(string userId)
        
            return _newsFeedService.GetNewsFeedItemsForUser(userId);
        

        [Route("user/userId/feeds/async")]
        [HttpGet]
        public async Task<IEnumerable<NewsFeedItem>> GetNewsFeedItemsForUserAsync(string userId)
        
            return await Task.Run(() => _newsFeedService.GetNewsFeedItemsForUser(userId));
        

看完Steve Sanderson's presentation后,我向每个端点发出了以下命令ab -n 100 -c 10 http://localhost....

我很惊讶,因为每个端点的基准似乎大致相同。

按照 Steve 的解释,我期待异步端点的性能更高,因为它会立即将线程池线程释放回线程池,从而使它们可用于其他请求并提高吞吐量。但数字似乎完全一样。

我在这里误会了什么?

【问题讨论】:

【参考方案1】:

使用 await Task.Run 创建 "async" WebApi 是个坏主意 - 您仍将使用线程,甚至来自 the same thread pool used for requests。

这会导致一些不愉快的时刻,详细描述here:

额外的(不必要的)线程切换到 Task.Run 线程池线程。同样,当该线程完成请求时,它必须 输入请求上下文(这不是实际的线程切换,而是 确实有开销)。 创建了额外的(不必要的)垃圾。异步编程是一种折衷:以更高的响应速度为代价获得更高的响应能力 内存使用情况。在这种情况下,您最终会为 完全没有必要的异步操作。 Task.Run “意外”借用了一个线程池线程,导致 ASP.NET 线程池启发式算法失效。我没有很多 这里有经验,但我的直觉告诉我,启发式 如果意外的任务真的很短并且会恢复得很好 如果意外任务持续超过两次,就不会优雅地处理它 秒。 ASP.NET 无法提前终止请求,即,如果客户端断开连接或请求超时。在同步情况下, ASP.NET 知道请求线程并且可以中止它。在里面 异步情况下,ASP.NET 不知道辅助线程池 线程是“为”该请求的。可以通过使用来解决此问题 取消令牌,但这超出了本博文的范围。

基本上,您不允许对 ASP.NET 进行任何异步 - 您只需将 CPU 绑定的同步代码隐藏在 async 外观后面。 Async 本身是 I/O 绑定代码的理想选择,因为它允许以最高效率利用 CPU(线程)(I/O 没有阻塞),但是当你有计算绑定代码时,你仍然会有以相同程度利用 CPU。

考虑到Task 的额外开销和上下文切换,您将获得比使用简单同步控制器方法更糟糕的结果。

如何实现真正的异步:

GetNewsFeedItemsForUser 方法应该变成async

    [Route("user/userId/feeds/async")]
    [HttpGet]
    public async Task<IEnumerable<NewsFeedItem>> GetNewsFeedItemsForUserAsync(string userId)
    
        return await _newsFeedService.GetNewsFeedItemsForUser(userId);
    

这样做:

如果它是某个库方法,则查找它的async 变体(如果没有 - 运气不好,您将不得不搜索一些与之竞争的类似物)。 如果它是您使用文件系统或数据库的自定义方法,则利用它们的异步工具为该方法创建异步 API。

【讨论】:

好的,我知道 Task.Run 在 ASP.NET 中是个坏主意,因为它使用线程池中的线程。我想我现在的问题是(1)我应该尝试将这项工作交给另一个线程吗?我对_newsFeedService.GetNewsFeedItemsForUser(userId) 的调用是对数据库的调用,我猜这是网络和I/O,而不是CPU 限制。 (2) 如果是个好主意,那段代码应该怎么写?我看到的所有示例都没有显示那部分代码。任何示例都会非常有用。 @SimonLomax 只需搜索“C# entity framework async” - msdn.microsoft.com/en-us/data/jj819165.aspx 如果您使用实体框架或“C# database async” -tugberkugurlu.com/archive/… 如果您使用标准 ADO。 我实际上使用的是 mongo 和 mongo 的 c# 驱动程序,而不是实体框架。无论如何,根据您的说法,除非我从自定义方法 _newsFeedService.GetNewsFeedItemsForUser(userId); 进行的调用是异步的,否则尝试使我的控制器操作方法异步是没有意义的。 @SimonLomax 似乎 MongoDB 有一些异步 API ***.com/questions/19740329/mongodb-net-async-await。但是您显然必须更改 GetNewsFeedItemsForUser 方法才能通过此 API 工作。它可能会成为一个不平凡但几乎不是很难的改变。

以上是关于具有 Task.Run 性能的 ASP.NET Web API 2 异步操作方法的主要内容,如果未能解决你的问题,请参考以下文章

Task.Run 不运行方法?

Asp.net core web Api Fire and Forget BG task

在 Task.Run() 中包装同步调用以使其异步有益吗?

Task.Run使用默认线程池

用于后台任务的asp.net框架中的Ihostedservice等价物

从 asp.net API 中的方法返回后,如何保持线程运行?