ASP.NET Web API 2 Async action methods with Task.Run performance

I’m trying to benchmark (using Apache bench) a couple of ASP.NET Web API 2.0 endpoints. One of which is synchronous and one async.

        [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));
        }

After watching Steve Sanderson’s presentation I issued the following command ab -n 100 -c 10 http://localhost.... to each endpoint.

I was surprised as the benchmarks for each endpoint seemed to be approximately the same.

Going off what Steve explained I was expecting that the async endpoint would be more performant because it would release thread pool threads back to the thread pool immediately, thus making them available for other requests and improving throughput. But the numbers seem exactly the same.

What am I misunderstanding here?

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

Using await Task.Run to create “async” WebApi is a bad idea – you will still use a thread, and even from the same thread pool used for requests.

It will lead to some unpleasant moments described in good details 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.

Basically, you do not allow any asynchrony to the ASP.NET – you just hide the CPU-bound synchronous code behind the async facade. Async on its own is ideal for I/O bound code, because it allows to utilize CPU (threads) at their top efficiency (no blocking for I/O), but when you have Compute-bound code, you will still have to utilize CPU to the same extent.

And taking into account the additional overhead from Task and context switching you will get even worser results than with simple sync controller methods.

HOW TO MAKE IT TRULY ASYNC:

GetNewsFeedItemsForUser method shall be turned into async.

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

To do it:

  • If it is some library method then look for its async variant (if there are none – bad luck, you’ll have to search for some competing analogue).
  • If it is your custom method using file system or database then leverage their async facilities to create async API for the method.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x