Search code examples
c#.nettaskwebapi

Is it viable to use a moderate lengthy operation to run inside a .NET Web API method


I read an excellent article about asynchronous tasks inside a Web API method, see link:

http://blog.tonysneed.com/2013/03/22/async-services-asp-net-web-api-entity-framework-6

Now for my solution (Framework 4.0) I have a third party tool which can transform an URL into an image. I like to use this inside a Web API method. Example pseudo code:

[HttpPost]
public HttpResponseMessage SaveSomething(...)
{
   SaveSomethingToDatabase(...);

   var image = ThirdPartyTool(someUrl);
   image.Save("someFullName");
}

Because the third party tool takes some time (3 to 6 seconds on average, the URL called loads a Google Maps layer) I like to know if I should embed this inside a Task (like the article describes, but then the third party tool should have some async support which it hasn't) or is Web API reliable enough to handle this kind of scenario's (regarding multiple repeated requests)?

A better way would be of course to create a windows service dedicated in creating images. But then I need to create a lot of extra stuff (a reliable image queue, a windows service an installer etc).


Solution

  • First off, ASP.NET 4.0 does not support async and await. Full stop. If you install Microsoft.Bcl.Async, you can get it to compile, but the behavior at runtime is undefined. End of story, sorry.


    But even if it was possible (e.g., after upgrading to ASP.NET 4.5), you still wouldn't want to do this. You should absolutely not wrap this in a Task.

    Consider the current synchronous behavior:

    • Request comes in; ASP.NET takes a thread from its thread pool and assigns it to the request.
    • Request is processed synchronously. Part of the time (within the ThirdPartyTool method), the request thread is blocked and idle.

    This is not ideal, because you do have a request thread that is blocked not doing anything useful. However, ASP.NET is designed to handle this scenario gracefully, since it is (today) quite common.

    Now, let's wrap it in a Task. Since the third-party tool is fully synchronous, the only option we really have is Task.Run or an equivalent:

    [HttpPost]
    public async Task<HttpResponseMessage> SaveSomething(...)
    {
      SaveSomethingToDatabase(...);
    
      var image = await Task.Run(() => ThirdPartyTool(someUrl));
      image.Save("someFullName");
    }
    

    Now, consider how this asynchronous request is handled:

    • Request comes in; ASP.NET takes a thread A from its thread pool and assigns it to the request.
    • When the handler hits Task.Run, it queues ThirdPartyTool to the thread pool. This takes another thread B from ASP.NET's thread pool and starts executing the code.
    • The handler awaits the task returned from Task.Run. This frees up the request thread A, allowing it to return to the ASP.NET thread pool.
    • When the second thread B completes ThirdPartyTool, it notifies the handler that its task is complete. The handler then executes Save (as an optimization, the remainder of the handler is actually executed on thread B instead of pulling another thread from the thread pool, but that's an implementation detail).

    If you think about it, it's definitely a performance pessimization. You're doing the same amount of work but also doing at least one extra context switch and messing with the ASP.NET thread pool heuristics twice by unexpectedly taking a task and then unexpectedly returning it later.

    For this reason, it's a good guideline to avoid Task.Run in ASP.NET.

    Wrapping code in Task.Run is what I call "fake asynchronous" - it acts asynchronous and looks asynchronous but underneath it's just queueing work to a thread pool thread.

    Now, if you have a truly asynchronous operation, it's a different story. Let's say that the third party upgrades its tool with asynchronous support, and they do it right. Then your handler could look like this:

    [HttpPost]
    public async Task<HttpResponseMessage> SaveSomething(...)
    {
      SaveSomethingToDatabase(...);
    
      var image = await ThirdPartyToolAsync(someUrl);
      image.Save("someFullName");
    }
    

    And this time the asynchronous request is handled completely differently:

    • Request comes in; ASP.NET takes a thread from its thread pool and assigns it to the request.
    • ThirdPartyToolAsync starts an asynchronous web request to Google (or whatever).
    • The handler awaits the task returned from ThirdPartyToolAsync. This frees up the request thread, allowing it to return to the ASP.NET thread pool.
    • There are now no threads executing this request. At all.
    • Later, the web request (or whatever) completes, and ASP.NET takes a thread from its thread pool to continue the handler.
    • That thread finishes ThirdPartyToolAsync and proceeds to finish SaveSomething.

    It's only with truly asynchronous methods like this that you get any benefit from async on ASP.NET, because you're reducing the pressure on the thread pool. Fake asynchronous methods increase the pressure, providing negative benefit.