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

Sim*_*max 15 c# asp.net task-parallel-library async-await asp.net-web-api

我正在尝试使用几个ASP.NET Web API 2.0端点进行基准测试(使用Apache工作台).其中一个是同步和一个异步.

        [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));
        }
Run Code Online (Sandbox Code Playgroud)

在观看了Steve Sanderson的演讲之后,ab -n 100 -c 10 http://localhost....向每个端点发出了以下命令.

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

关闭史蒂夫解释说我期待异步端点更高效,因为它会立即将线程池线程释放回线程池,从而使它们可用于其他请求并提高吞吐量.但数字看起来完全一样.

我在这里误解了什么?

Eug*_*kal 19

利用await Task.Run创建"异步"的WebAPI是一个坏主意-你仍然会使用一个线程,甚至用于请求相同的线程池.

这将导致一些不愉快的时刻在这里详细描述:

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

基本上,您不允许任何与ASP.NET的异步 - 您只需隐藏异步外观后面的CPU绑定同步代码.Async它本身是I/O绑定代码的理想选择,因为它允许以最高效率利用CPU(线程)(不阻塞I/O),但是当你有Compute绑定代码时,你仍然必须利用CPU达到同样的程度.

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

如何使其成为真正的ASYNC:

GetNewsFeedItemsForUser方法应该变成async.

    [Route("user/{userId}/feeds/async")]
    [HttpGet]
    public async Task<IEnumerable<NewsFeedItem>> GetNewsFeedItemsForUserAsync(string userId)
    {
        return await _newsFeedService.GetNewsFeedItemsForUser(userId);
    }
Run Code Online (Sandbox Code Playgroud)

去做吧:

  • 如果它是一些库方法,那么寻找它的async变体(如果没有 - 运气不好,你将不得不寻找一些竞争模拟).
  • 如果它是使用文件系统或数据库的自定义方法,则利用其异步工具为该方法创建异步API.