Async operations on ASP.NET

Asked

Viewed 1,921 times

9

When it is advantageous to use asynchronous operations in an application ASP.NET (classic, MVC, MVC Web API)?

When do I use the ThreadPool to perform some operations may be useful?

2 answers

18


Async on the Web

HTTP is a protocol fundamentally synchronous. The client sends a request -> the server processes the request -> the server sends a reply -> the client receives the reply.

The goal when developing an ASP.NET web application (classic, MVC, MVC Web API) is to try to process the request in a single thread. The more thread changes there are, the worse the performance will be - every time you change threads, the CPU has to save the current state of a thread (stack, program counter, etc.), and recover the state of the other thread. This process is called "context switch" and is an expensive and heavy process. It is time that the processor spends without performing useful work.

For example, imagine the code:

ProcessamentoInicial();
await Task.Run(AlgoritmoIntensivo);
ProcessamentoFinal();
  1. With the await, we are releasing the HTTP request thread (thread A), and running the algorithm on a Threadpool thread (thread B).
  2. While B is busy, the thread A is free to do another job (serve other requests that reach the server, some pending work in Threadpool, etc)
  3. When the thread B complete, the final processing will be scheduled to run on thread A again, and send the reply to the customer.

Without await Task.Run, the code would look like this:

ProcessamentoInicial();
AlgoritmoIntensivo(); //bloqueante
ProcessamentoFinal();

In this case, all code runs synchronously on the same thread.

Let’s visualize the behavior of these 2 solutions, when we have 2 threads (X and Y) serving 2 requests (R1 and R2) arriving at the server simultaneously.

Diagrama de processamento CPU-bound síncrono

As we can see, in the asynchronous version, the thread X deferred the execution of the algorithm to the thread Y, and the thread Y deferred to the thread X. This is because ASP.NET and Task.Run use the same Threadpool. So threads interfere with each other.

Because of overhead Introduced by context changes, the "asynchronous" version performed worse. Both requests are served faster if we process everything in a synchronous way.

Disadvantages of the "asynchronous version":

  1. "Unnecessary context switches "
  2. Unnecessary junk (due to the use of the async/await language, the compiler generates a status machine to manage the task)
  3. The algorithm used by ASP.NET temporarily destabilizes (because Task.Run borrowed a thread "unexpectedly"). This algorithm constantly calculates server throughput and makes small optimizations to increase this throughput. Interfering with this algorithm is.... unwanted.
  4. If the client disconnects, the server will not be able to abort the order processing because we have broken the synchronous flow.

Async I/O

However, there is an exception! Unlike operations CPU-bound (CPU work-dependent operations such as math calculations, intensive loops, etc), operations I/O-bound (operations that rely on external communication, such as database calls, web services, read files on disk, etc) must be asynchronous!

Unlike the previous example, when using asynchronous I/O, none of the threads are blocked. Instead, process threads are free, and when operation I/O has completed, the "I/O Completion port" receives a signal interrupt from the I/O. hardware One can think of a I/O Completion port as a thread lightweight dedicated only to I/O, and to nothing else.

Let’s see:

ProcessamentoInicial();
await BD.ExecutarQueryAsync(); 
ProcessamentoFinal();

Diagrama de I/O assíncrona

Now, as both threads defer execution to the Completion ports, they don’t interfere with each other - both are truly free to process other requests, increasing server throughput and scalability.

But async I/O is not something that can be simulated in C#.

await Task.Run(() => DB.ExecutarQueryBloqueante()); will have the same disadvantages as the first example used Task.Run to run the algorithm. This is a common error.

Async I/O has to be supported by native drivers in the case of databases, or by the operating system in the case of read files on disk. These Apis should expose non-blocking methods (eg: DbCommand.ExecuteReaderAsync, StreamReader.ReadAsync). These Apis use I/O Completion ports instead of threads.

tl;dr

  1. In ASP.NET, all work cpu-bound should be processed synchronously, without creating new threads or deferring to Threadpool.
  2. For I/O operations, the asynchronous (non-blocking) API shall be used, if available.

References:

  • +1 nice explanation... however there are cases where it makes sense with CPU processing bound as well. I added an answer to cover this case.

9

A utility I see for bound CPU operations, would be running multiple processing on separate processors, with a await in the code like this:

var tasks = PegarMultiplasTarefas();
await Task.WhenAll(tasks);

This escapes the @dcastro, but is a valid point too, without necessarily being I/O.

In doing so, it distributes independent processing among the available processors. However, one has to consider whether this is really desirable, after all, once these processors are busy, they will have their ability to meet requests reduced.

This is a problem if your system meets various types of requests, both light processing and more intensive processing. If all processors get busy, then other shorter requests, which would be promptly met, will be executed more slowly as they will be dividing the processing time with other threads.

If the system aims to meet more intense processing requests, then it is worth dividing the processing of each request among the processors if possible. This is as follows: in a 4-processor system, if a linear processing request would take 4s, and it was possible to divide that processing into the 4 processors, so that in the end it was with 1s... then in 4s it would be possible to meet 4 requests... one would reply time t=1s, another in time t=2s, in the t=3s and another in the t=4s. Already if the request processing is not distributed among threads, it would be possible to meet 4 at the same time, and all would be answered in t=4s which in this case is a disadvantage.

  • I forgot to include that case, but the same logic applies. The threads occupied by these tasks could be used to serve other requests. In other words, scalability (number of customers we can serve per minute) is exchanged for performance (time required to serve a customer). The performance can be useful when there is little traffic in the server, but in situations of high traffic the scalability is' more important. Brad Wilson comments at the bottom of this post: http://www.hanselman.com/blog/TheMagicOfUsingAsynchronousMethodsInASPNET45PlusAnImportantGotcha.aspx

  • 1

    I agree with you in part. Scalability can be achieved by adding more machines (a farm), and performance can also be achieved with more machines (delegating the difficult-to-process task to a dedicated server). In this case, there is no change from scalability to performance, there is only one different way to organize processing.

  • I also partially agree :P My logic applies to each machine individually. I think the two logics fit together - by adding more machines, as in a web farm, we’re lowering traffic on each individual machine - so we’re lowering the resources used. In this case, it makes sense to use excess resources to improve the performance of each request.

  • If you add that trade-off to the answer, and the pros and cons, I give +1 too ;)

  • 1

    It is highly debatable that the work to be done to serve an HTTP request is parallelizable CPU work. Doing so may be the right solution to the wrong problem. Given the caveats, it is a tool to have in the toolbox of each, but nothing that should be done right from the start, by the yes - by the no.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.