12

I am actually reading some topics about the Task Parallel Library and the asynchronous programming with async and await. The book "C# 5.0 in a Nutshell" states that when awaiting an expression using the await keyword, the compiler transforms the code into something like this:

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

Let's assume, we have this asynchronous function (also from the referred book):

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!");
}

The call of the 'GetPrimesCountAsync' method will be enqueued and executed on a pooled thread. In general invoking multiple threads from within a for loop has the potential for introducing race conditions.

So how does the CLR ensure that the requests will be processed in the order they were made? I doubt that the compiler simply transforms the code into the above manner, since this would decouple the 'GetPrimesCountAsync' method from the for loop.

Dennis Kassel
  • 2,726
  • 4
  • 19
  • 30
  • 1
    It creates a state machine. Also that code is not going to run in parallel it will simply free up the thread while it waits for each call to `GetPrimesCountAsync` to complete in order. – juharr Jul 29 '15 at 16:03

2 Answers2

24

Just for the sake of simplicity, I'm going to replace your example with one that's slightly simpler, but has all of the same meaningful properties:

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

The ordering is all maintained because of the definition of your code. Let's imagine stepping through it.

  1. This method is first called
  2. The first line of code is the for loop, so i is initialized.
  3. The loop check passes, so we go to the body of the loop.
  4. SomeExpensiveComputation is called. It should return a Task<T> very quickly, but the work that it'd doing will keep going on in the background.
  5. The rest of the method is added as a continuation to the returned task; it will continue executing when that task finishes.
  6. After the task returned from SomeExpensiveComputation finishes, we store the result in value.
  7. value is printed to the console.
  8. GOTO 3; note that the existing expensive operation has already finished before we get to step 4 for the second time and start the next one.

As far as how the C# compiler actually accomplishes step 5, it does so by creating a state machine. Basically every time there is an await there's a label indicating where it left off, and at the start of the method (or after it's resumed after any continuation fires) it checks the current state, and does a goto to the spot where it left off. It also needs to hoist all local variables into fields of a new class so that the state of those local variables is maintained.

Now this transformation isn't actually done in C# code, it's done in IL, but this is sort of the morale equivalent of the code I showed above in a state machine. Note that this isn't valid C# (you cannot goto into a a for loop like this, but that restriction doesn't apply to the IL code that is actually used. There are also going to be differences between this and what C# actually does, but is should give you a basic idea of what's 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();
    }
}

Note that I've ignored task cancellation for the sake of this example, I've ignored the whole concept of capturing the current synchronization context, there's a bit more going on with error handling, etc. Don't consider this a complete implementation.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Thank you for your answer. As I understand it, the for loop is processed very quickly, thus creating 10 awaiters (tasks) to await for. The only continuation here is the call to 'Console.WriteLine'. So how does the CLR guarantee that the for loop will be continued only when the Task has been completed? – Dennis Kassel Jul 29 '15 at 16:18
  • Because that's how the `OnCompleted` method of the awaiter works. It is only fired when the operation it represents completes. In this case, the awaiter is the `Task` awaiter, so effectively it's just the implementation of the `Task` class that calls the method after the `Task` it represents is actually done, and not before then. It has nothing to do with the CLR. – Servy Jul 29 '15 at 16:20
  • I think I understand it now. The whole for loop (as well as its state) is part of the continuation. Therefore the for loop won't be continued until every single awaiter has completed step by step. This is exactly as I thought it to be, because only in this way the state of the for loop is embedded into the continuation, thus allowing the tasks to execute in the correct order. – Dennis Kassel Jul 29 '15 at 16:33
8

The call of the 'GetPrimesCountAsync' method will be enqueued and executed on a pooled thread.

No. await does not initiate any kind of background processing. It waits for existing processing to complete. It is up to GetPrimesCountAsync to do that (e.g. using Task.Run). It's more clear this way:

var myRunningTask = GetPrimesCountAsync();
await myRunningTask;

The loop only continues when the awaited task has completed. There is never more than one task outstanding.

So how does the CLR ensure that the requests will be processed in the order they were made?

The CLR is not involved.

I doubt that the compiler simply transforms the code into the above manner, since this would decouple the 'GetPrimesCountAsync' method from the for loop.

The transform that you shows is basically right but notice that the next loop iteration is not started right away but in the callback. That's what serializes execution.

usr
  • 168,620
  • 35
  • 240
  • 369
  • Thank you. This is also a very helpful answer. You are right: The await keyword only expects an object implementing 'INotifyCompletion' to complete. It is up to the implementation of 'GetResult' to execute it on a thread pool thread. – Dennis Kassel Jul 29 '15 at 16:41