ASP.NET Web API 2 个具有 Task.Run 性能的异步操作方法
ASP.NET Web API 2 Async action methods with Task.Run performance
我正在尝试(使用 Apache 工作台)对几个 ASP.NET Web API 2.0 端点进行基准测试。其中一个是同步的,一个是异步的。
[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 的解释,我期望异步端点的性能更高,因为它会立即将线程池线程释放回线程池,从而使它们可用于其他请求并提高吞吐量。但是数字看起来完全一样。
我误会了什么?
使用 await Task.Run
创建 "async" WebApi 是一个坏主意 - 您仍然会使用线程,甚至来自 the same thread pool used for requests。
这会导致一些不愉快的时刻,详细描述 here:
- Extra (unnecessary) thread switching to the Task.Run thread pool thread. Similarly, when that thread finishes the request, it has to
enter the request context (which is not an actual thread switch but
does have overhead).
- Extra (unnecessary) garbage is created. Asynchronous programming is a tradeoff: you get increased responsiveness at the expense of higher
memory usage. In this case, you end up creating more garbage for the
asynchronous operations that is totally unnecessary.
- The ASP.NET thread pool heuristics are thrown off by Task.Run “unexpectedly” borrowing a thread pool thread. I don’t have a lot of
experience here, but my gut instinct tells me that the heuristics
should recover well if the unexpected task is really short and would
not handle it as elegantly if the unexpected task lasts more than two
seconds.
- ASP.NET is not able to terminate the request early, i.e., if the client disconnects or the request times out. In the synchronous case,
ASP.NET knew the request thread and could abort it. In the
asynchronous case, ASP.NET is not aware that the secondary thread pool
thread is “for” that request. It is possible to fix this by using
cancellation tokens, but that’s outside the scope of this blog post.
基本上,您不允许对 ASP.NET 进行任何异步 - 您只需将 CPU 绑定的同步代码隐藏在异步外观后面。 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
变体(如果有 none - 运气不好,您将不得不搜索一些竞争类似物)。
- 如果您的自定义方法使用文件系统或数据库,则利用它们的异步功能为该方法创建异步 API。
我正在尝试(使用 Apache 工作台)对几个 ASP.NET Web API 2.0 端点进行基准测试。其中一个是同步的,一个是异步的。
[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 的解释,我期望异步端点的性能更高,因为它会立即将线程池线程释放回线程池,从而使它们可用于其他请求并提高吞吐量。但是数字看起来完全一样。
我误会了什么?
使用 await Task.Run
创建 "async" WebApi 是一个坏主意 - 您仍然会使用线程,甚至来自 the same thread pool used for requests。
这会导致一些不愉快的时刻,详细描述 here:
- Extra (unnecessary) thread switching to the Task.Run thread pool thread. Similarly, when that thread finishes the request, it has to enter the request context (which is not an actual thread switch but does have overhead).
- Extra (unnecessary) garbage is created. Asynchronous programming is a tradeoff: you get increased responsiveness at the expense of higher memory usage. In this case, you end up creating more garbage for the asynchronous operations that is totally unnecessary.
- The ASP.NET thread pool heuristics are thrown off by Task.Run “unexpectedly” borrowing a thread pool thread. I don’t have a lot of experience here, but my gut instinct tells me that the heuristics should recover well if the unexpected task is really short and would not handle it as elegantly if the unexpected task lasts more than two seconds.
- ASP.NET is not able to terminate the request early, i.e., if the client disconnects or the request times out. In the synchronous case, ASP.NET knew the request thread and could abort it. In the asynchronous case, ASP.NET is not aware that the secondary thread pool thread is “for” that request. It is possible to fix this by using cancellation tokens, but that’s outside the scope of this blog post.
基本上,您不允许对 ASP.NET 进行任何异步 - 您只需将 CPU 绑定的同步代码隐藏在异步外观后面。 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
变体(如果有 none - 运气不好,您将不得不搜索一些竞争类似物)。 - 如果您的自定义方法使用文件系统或数据库,则利用它们的异步功能为该方法创建异步 API。