Async and Await - How is the execution order maintained?

I really read several topics about the parallel task library and asynchronous programming with async and wait. The book β€œC # 5.0 in a nutshell” says that when waiting for an expression using the await keyword, the compiler converts the code into something like this:

var awaiter = expression.GetAwaiter(); awaiter.OnCompleted (() => { var result = awaiter.GetResult(); 

Suppose we have this asynchronous function (also from the book mentioned):

 async Task DisplayPrimeCounts() { for (int i = 0; i < 10; i++) Console.WriteLine (await GetPrimesCountAsync (i*1000000 + 2, 1000000) + " primes between " + (i*1000000) + " and " + ((i+1)*1000000-1)); Console.WriteLine ("Done!"); } 

A call to the GetPrimesCountAsync method will be queued and executed in a federated thread. In general, using multiple threads from a for loop has the potential to introduce race conditions.

So how does the CLR ensure that requests are processed in the order in which they were made? I doubt that the compiler will simply convert the code to the above method, as this will remove the GetPrimesCountAsync method from the for loop.

+7
multithreading c # asynchronous task-parallel-library async-await
source share
2 answers

Just for simplicity, I'm going to replace your example with a slightly simpler one, but it has the same significant properties:

 async Task DisplayPrimeCounts() { for (int i = 0; i < 10; i++) { var value = await SomeExpensiveComputation(i); Console.WriteLine(value); } Console.WriteLine("Done!"); } 

All orders are saved due to the definition of your code. Imagine going through it.

  • This method is first called
  • The first line of code is the for loop, so i initialized.
  • The cycle check passes, so we move on to the body of the cycle.
  • SomeExpensiveComputation . It should return Task<T> very quickly, but the work that it will do will continue in the background.
  • The rest of the method is added as a continuation of the returned task; he will continue execution when this task is completed.
  • After completing the task returned from SomeExpensiveComputation , we save the result in value .
  • value is displayed on the console.
  • Goto 3; note that the existing expensive operation is already completed before we go to step 4 a second time and start the next one.

As far as the C # compiler really performs step 5, it does this by creating a state machine. Basically, every time there is await , there is a label indicating where it was stopped, and at the beginning of the method (or after resuming after any continuation), it checks the current state and makes goto in the place where it stopped. It is also necessary to raise all local variables into the fields of the new class in order to preserve the state of these local variables.

Now this conversion is not actually performed in C # code, it is done in IL, but it is a kind of moral equivalent to the code that I showed above in the final machine. Note that this is not valid C # (you cannot goto in aa for like this, but this restriction does not apply to the IL code used. There are also differences between this and that C # in fact, but should give you general idea of ​​what is going on here:

 internal class Foo { public int i; public long value; private int state = 0; private Task<int> task; int result0; public Task Bar() { var tcs = new TaskCompletionSource<object>(); Action continuation = null; continuation = () => { try { if (state == 1) { goto state1; } for (i = 0; i < 10; i++) { Task<int> task = SomeExpensiveComputation(i); var awaiter = task.GetAwaiter(); if (!awaiter.IsCompleted) { awaiter.OnCompleted(() => { result0 = awaiter.GetResult(); continuation(); }); state = 1; return; } else { result0 = awaiter.GetResult(); } state1: Console.WriteLine(value); } Console.WriteLine("Done!"); tcs.SetResult(true); } catch (Exception e) { tcs.SetException(e); } }; continuation(); } } 

Please note that I ignored the task cancellation for this example, I ignored the whole concept of capturing the current synchronization context, there is a bit more error handling, etc. Do not consider this a complete implementation.

+14
source share

A call to the GetPrimesCountAsync method will end and run in the federated thread.

Not. await does not initiate any background processing. He is waiting for the completion of existing processing. To do this, execute GetPrimesCountAsync (for example, using Task.Run ). This is more clear:

 var myRunningTask = GetPrimesCountAsync(); await myRunningTask; 

The cycle continues only after completion of the expected task. There is no more than one task.

So how does the CLR ensure that requests are processed in the order in which they were made?

CLR is not involved.

I doubt that the compiler will simply convert the code to the above method, as this will remove the GetPrimesCountAsync method from the for loop.

The conversion you are showing is basically correct, but note that the next iteration of the loop does not start immediately, but in the callback. What serializes execution.

+7
source share

All Articles