Question

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
Was it helpful?

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.

OTHER TIPS

As soon as you call the following line in your ProcessMany, each call to ProcessMany starts executing in a separate thread in a separate thread pool right away.

await ...;

So that's why you see a bunch of calls before your "Processing" printout. So while you have all those 10 ProcessMany calls executing, then you start running your large loop. As that large loop is running on your main thread, the 10 ProcessMany calls continue to execute in their threads, producing the additional printouts. Looks like your ProcessMany calls do not finish executing before your main thread loop, so they continue to spit out more results after your "Done Processing" printout.

I hope that clarifies the order of things for you.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top