Deep understanding of async / await on ASP.NET MVC

22,486

Solution 1

There's some good resources on the 'net that do describe this in detail. I wrote an MSDN article that describes this at a high level.

What i do not understand is why the request is still alive and waits for an answer because in the end the calling client will wait for our request to complete.

It's still alive because the ASP.NET runtime has not yet completed it. Completing the request (by sending the response) is an explicit action; it's not like the request will complete on its own. When ASP.NET sees that the controller action returns a Task/Task<T>, it will not complete the request until that task completes.

My question is: Who / when / how does this wait for complete blocking occurs ?

Nothing is waiting.

Think of it this way: ASP.NET has a collection of current requests that it's processing. For a given request, as soon as it's complete, the response is sent out and then that request is removed from the collection.

The key is that it's a collection of requests, not threads. Each of those requests may or may not have a thread working on it at any point in time. Synchronous requests always have a single thread (the same thread). Asynchronous requests may have periods when they don't have threads.

Note: i saw this thread: http://blog.stephencleary.com/2013/11/there-is-no-thread.html and it makes sense for GUI applications but for this server side scenario I don't get it.

The threadless approach to I/O works exactly the same for ASP.NET apps as it does for GUI apps.

Eventually, the file write will complete, which (eventually) completes the task returned from ReadFile. This "completing of the task" work is normally done with a thread pool thread. Since the task is now complete, the Upload action will continue executing, causing that thread to enter the request context (that is, there is now a thread executing that request again). When the Upload method is complete, then the task returned from Upload is complete, and ASP.NET writes out the response and removes the request from its collection.

Solution 2

Under the hood the compiler performs a sleight of hand and transforms your async \ await code into a Task-based code with a callback. In the most simple case:

public async Task X()
{
    A();
    await B();
    C();
}

Gets changed into something like:

public Task X()
{
    A();
    return B().ContinueWith(()=>{ C(); })
}

So there's no magic - just a lot of Tasks and callbacks. For more complex code the transformations will be more complex too, but in the end the resulting code will be logically equivalent to what you wrote. If you want, you can take one of ILSpy/Reflector/JustDecompile and see for yourself what is compiled "under the hood".

ASP.NET MVC infrastructure in turn is intelligent enough to recognize if your action method is a normal one, or a Task based one, and alter its behavior in turn. Therefore the request doesn't "disappear".

One common misconception is that everything with async spawns another thread. In fact, it's mostly the opposite. At the end of the long chain of the async Task methods there is normally a method which performs some asynchronous IO operation (such as reading from disk or communicating via network), which is a magical thing performed by Windows itself. For the duration of this operation, there is no thread associated with the code at all - it's effectively halted. After the operation completes however, Windows calls back and then a thread from the thread pool is assigned to continue the execution. There's a bit of framework code involved to preserve the HttpContext of the request, but that's all.

Solution 3

The ASP.NET runtime understands what tasks are and delays sending the HTTP response until the task is done. In fact the Task.Result value is needed in order to even generate a response.

The runtime basically does this:

var t = Upload(...);
t.ContinueWith(_ => SendResponse(t));

So when your await is hit both your code and the runtimes code gets off the stack and "there is no thread" at that point. The ContinueWith callback revives the request and sends the response.

Share:
22,486
George Lica
Author by

George Lica

Passionate Software Engineer with several years in the IT industry. Learning new things every day is a way of life for me and I enjoy coding and designing the best solutions for any kind of problems that we might face.

Updated on September 15, 2020

Comments

  • George Lica
    George Lica over 3 years

    I don't understand exactly what is going on behind the scenes when I have an async action on an MVC controller especially when dealing with I/O operations. Let's say I have an upload action:

    public async Task<ActionResult> Upload (HttpPostedFileBase file) {
      ....
      await ReadFile(file);
    
      ...
    }
    

    From what I know these are the basic steps that happen:

    1. A new thread is peeked from threadpool and assigned to handle incomming request.

    2. When await gets hit, if the call is an I/O operation then the original thread gets back into pool and the control is transfered to a so-called IOCP (Input output completion port). What I do not understand is why the request is still alive and waits for an answer because in the end the calling client will wait for our request to complete.

    My question is: Who / when / how does this wait for complete blocking occurs?

    Note: I saw the blog post There Is No Thread, and it makes sense for GUI applications, but for this server side scenario I don't get it. Really.

  • Sully
    Sully over 7 years
    "The key is that it's a collection of requests, not threads. Each of those requests may or may not have a thread working on it at any point in time. Synchronous requests always have a single thread (the same thread). Asynchronous requests may have periods when they don't have threads." Clears it up nicely.