Search code examples
c#async-awaitcontinuations

Using async/await, when do continuations happen?


I am attempting to use async/await in a very large already existing synchronous code base. There is some global state in this code base that works fine, if kludgy, in a synchronous context, but it doesn't work in the asynchronous context of async/await.

So, my two options seem to be to either factor out the global context which woould be a very large and very time consuming task, or do something clever with when continuations run.

In order to better understand async/await and continuations, I made a test program, shown here. Shown here.

    // A method to simulate an Async read of the database.
    private static Task ReadAsync()
    {
        return Task.Factory.StartNew(() =>
        {
            int max = int.MaxValue / 2;
            for (int i = 0; i < max; ++i)
            {
            }
        });
    }

    // An async method that creates several continuations.
    private static async Task ProcessMany(int i)
    {
        Console.WriteLine(string.Format("{0} {1}", i.ToString(), 0));

        await ReadAsync();

        Console.WriteLine(string.Format("{0} {1}", i.ToString(), 1));

        await ReadAsync();

        Console.WriteLine(string.Format("{0} {1}", i.ToString(), 2));

        await ReadAsync();

        Console.WriteLine(string.Format("{0} {1}", i.ToString(), 3));
    }


    public static void Main(string[] args)
    {
        Queue<Task> queue = new Queue<Task>();

        for (int i = 0; i < 10; ++i)
        {
            queue.Enqueue(ProcessMany(i));
        }

        // Do some synchonous processing...
        Console.WriteLine("Processing... ");

        for (int i = 0; i < int.MaxValue; ++i)
        {
        }

        Console.WriteLine("Done processing... ");

        queue.Dequeue().Wait();
    }

After reading all about async/await, my understanding would be that none of the continuations would happen between the "Processing.. " and "Done processing... " WriteLines.

Here is some sample output.

0 0
1 0
2 0
3 0
4 0
5 0
6 0
7 0
8 0
9 0
Processing...
3 1
2 1
7 1
6 1
0 1
4 1
5 1
1 1
6 2
3 2
Done processing...
7 2
2 2
0 2
4 2
5 2
1 2
6 3
3 3
7 3
2 3
0 3

I would expect the single Wait() at the end of the program to potentially yield to multiple continuations while the first one finishes, but I don't understand how any continuations could run between the "Processing... " and the "Done Processing... ". I thought there might be a yield or something in the Console.WriteLine method, so I completely replaced it, but that didn't change the output.

There is clearly a gap in my understanding of async/await. How could a continuation happen when we are simply incrementing a variable? Is the compiler or CLR injecting some sort of magic here?

Thank you in advance for any help in better understanding async/await and continuations.

EDIT:

If you edit the sample code this way as per the comment by Stephen, what's going is much more obvious.

    // An async method that creates several continuations.
    private static async Task ProcessMany(int i)
    {
        Console.WriteLine(string.Format("{0} {1} {2}", i.ToString(), 0, Thread.CurrentThread.ManagedThreadId));

        await ReadAsync();

        Console.WriteLine(string.Format("{0} {1} {2}", i.ToString(), 1, Thread.CurrentThread.ManagedThreadId));

        await ReadAsync();

        Console.WriteLine(string.Format("{0} {1} {2}", i.ToString(), 2, Thread.CurrentThread.ManagedThreadId));

        await ReadAsync();

        Console.WriteLine(string.Format("{0} {1} {2}", i.ToString(), 3, Thread.CurrentThread.ManagedThreadId));
    }


    public static void Main(string[] args)
    {
        Queue<Task> queue = new Queue<Task>();

        for (int i = 0; i < 10; ++i)
        {
            queue.Enqueue(ProcessMany(i));
        }

        // Do some synchonous processing...
        Console.WriteLine("Processing... {0}", Thread.CurrentThread.ManagedThreadId);

        for (int i = 0; i < int.MaxValue; ++i)
        {
        }

        Console.WriteLine("Done processing... {0}", Thread.CurrentThread.ManagedThreadId);

        queue.Dequeue().Wait();
    }

Output:

0 0 9
1 0 9
2 0 9
3 0 9
4 0 9
5 0 9
6 0 9
7 0 9
8 0 9
9 0 9
Processing... 9
4 1 14
3 1 13
2 1 12
5 1 15
0 1 10
6 1 16
1 1 11
7 1 17
4 2 14
3 2 13
0 2 10
6 2 16
2 2 12
5 2 15
Done processing... 9
1 2 11
7 2 17
0 3 10
4 3 14

Solution

  • If you don't have a current SynchronizationContext or TaskScheduler, then the continuations will execute on a thread pool thread (separately from the main thread). This is the case in Console apps but you'll see very different behavior in WinForms/WPF/ASP.NET.

    While you could control the continuation scheduling by using a custom TaskScheduler, that would be quite a bit of work with probably very little benefit. I'm not clear on what the problems are with your global state, but consider alternatives such as SemaphoreSlim.