Mark Gilbert's Blog

Science and technology, served light and fluffy.

Wait your turn! Async and Await

It’s all about the bubbles.  Let me explain.

I was trying to implement something that I thought would be more efficiently done asynchronously.  Since I was using .NET 4.0 with the "Async for .NET Framework 4" NuGet package, I had access to the new "async" and "await" keywords.  These were designed to make it easier to kick off an asynchronous operation.  I was about to find out, however, that these weren’t silver bullets – I still had to understand what was going on to use them properly.

For illustrative purposes only, I’ll use a boiled-down version of what I was trying to implement.  This is a simple console application that tries to do some “work” (really just some calls to Thread.Sleep()), and records the order that it implements the steps.

10

First, the simple case – good ole’ fashioned synchronous:

        private static void Option1()
        {
            Option1_Step1();
            Console.WriteLine("3");
            Option1_Step2();
            Console.WriteLine("6");
        }
        private static void Option1_Step1()
        {
            Console.WriteLine("1");
            DoSomeSychronousWork(500);
            Console.WriteLine("2");
        }
        private static void Option1_Step2()
        {
            Console.WriteLine("4");
            DoSomeSychronousWork(500);
            Console.WriteLine("5");
        }

The DoSomeSynchronousWork() routine looks like this:

        private static void DoSomeSychronousWork(int Milliseconds)
        {
            System.Threading.Thread.Sleep(Milliseconds);
            return;
        }

Which outputs the checkpoint numbers 1-6 in order:

20

My first attempt to run Step 1 and Step 2 asynchronously, however, didn’t work out as I had expected:

        private static void Option2()
        {
            Option2_Step1();
            Console.WriteLine("3");
            Option2_Step2();
            Console.WriteLine("6");
        }
        private static async void Option2_Step1()
        {
            Console.WriteLine("1");
            await DoSomeAsynchronousWork(500);
            Console.WriteLine("2");
        }
        private static async void Option2_Step2()
        {
            Console.WriteLine("4");
            await DoSomeAsynchronousWork(500);
            Console.WriteLine("5");
        }

My initial, naive interpretation of "asynchronous operations don’t block the thread" was that if I slapped the "await" keyword onto a method call, it would execute that method on a new thread, and put the rest of my application to sleep, freeing the thread that it had been on to be used by something else on the computer.  Then, when my “awaitable" method returned, the computer would wake my program back up to continue where it left off.  No blocking, right?

I’m still not certain how right this interpretation was, but it definitely was not 100%.  I doubt I even made it past the 50s.  Here is what my program was now outputting:

30

Excellent.  Let’s just execute all of that methods that we can as fast as we can, and the awaitable ones will just catch up.  Mmmm… yeah, that’s really not going to work for me.

As I dug into it, and consulted Stephen Cleary’s excellent post, "Async and Await", I managed to piece together what was going on here.

 

First, the Main() routine calls Option2(), and execution begins.

32

Control then goes to the first step – Option2_Step1().

33

Which writes out the first checkpoint.  Next, it begins execution of the asynchronous work item.

34

The DoSomeAsynchronousWork() function is as follows:

        private static Task DoSomeAsynchronousWork(int Milliseconds)
        {
            return (new TaskFactory()).StartNew(() => System.Threading.Thread.Sleep(Milliseconds));
        }

Now, the work here has only STARTED.  Sync we said wanted to await this method, execution of Option2_Step1() will now be paused, waiting for DoSomeAsynchronousWork to return.  Control will now be passed back to the calling function, Option2().

35

Where it will charge forward onto the next piece of logic, checkpoint #3.  Then it will continue on to Option2_Step2().

36

Where it will then write out checkpoint #4, and start yet another asynchronous task.

37

With that second asynchronous task started, it will immediately return to the calling function, Option2(), and continue on by printing out checkpoint #6.

38

With Option2() now complete, it returns to the Main() function, and where it prints out the "Press [Enter] to run again." prompt, and waits for the user to press "Enter".

39

A short time later, the first asynchronous task, started in Option2_Step1(), completes and execution is started back up for that method, which results in checkpoint #2 being written out.  Then finally, the asynchronous task that was started in Option2_Step2() completes, and execution is started back up for that method, which results in checkpoint #5 being written out.

***

What I came to realize was that the methods I was using "async" on – Option2_Step1() and Option2_Step2() – formed a kind of "bubble".  Things within the bubble would be executed serially, but asynchronously. 

The serial part means that checkpoint 1 would always be reached out before checkpoint 2, and checkpoint 4 would always be reached before checkpoint 5. 

The asynchronous part means when the execution reaches the first work item (between checkpoints 1 and 2) the await keyword tells .NET to start that work, AND THEN RETURN AND KEEP GOING with the rest of the program – in this case, it would return to the Option2() method, print out checkpoint 3, etc.  In other words, when things inside the bubble are paused, control is passed back, to whatever called the bubble.

This is where the "doesn’t block" facet comes into play – when something asynchronous is started .NET will return control to the calling method, putting the current method on pause (so to speak).  When the work item is finished, .NET will start that method back up again, right where it left off.

So, how can I ensure that Step1 will complete before Step2 does?  The solution I landed on was to move the asynchronous nature of this program up a level:

        private static async void Option3()
        {
            await Option3_Step1();
            Console.WriteLine("3");
            await Option3_Step2();
            Console.WriteLine("6");
        }
        private static async Task Option3_Step1()
        {
            Console.WriteLine("1");
            await DoSomeAsynchronousWork(500);
            Console.WriteLine("2");
        }
        private static async Task Option3_Step2()
        {
            Console.WriteLine("4");
            await DoSomeAsynchronousWork(500);
            Console.WriteLine("5");
        }

Here, I’ve declared Option3() – the top level function – async as well.  With that in place, Option3_Step1() and Option3_Step2() should be called serially.  As before, Main() calls Option3(), and we begin by calling Option3_Step1().

42

That immediately leads to the first checkpoint.

43

And the first asynchronous piece of work.

44

Which means execution of Option3_Step1() is now paused, and control is returned to the calling function, Option3().  However, since we’re awaiting Optino3_Step1(), control is passed out ANOTHER level, back to Main(), where it prints out the "Press [Enter] to run again." prompt, and waits for the user to press "Enter".  At that point, there is nothing more that can be done, so the entire program waits.

When our first bit of asynchronous work completes, it picks back up where it left off Option3_Step1():

45

Which means checkpoint 2 is now printed out.  That’s the end of Option3_Step1(), so it returns to Option3(), prints out checkpoint 3, and begins execution of Option3_Step2():

46

Checkpoint 4 is printed out, and then the second bit of asynchronous work is started.

47

Again, execution is paused here, and control is returned to the calling function, Option3(), and then paused there and again passed back to Main().  Since the rest of Main() has already executed, nothing more happens.  The program is simply paused until the second piece of asynchronous work is completed.

When it completes, Option3_Step2() picks back up, and checkpoint 5 is written out.

48

And then checkpoint 6.

49

Here is the output, in aggregate:

40

So, by making Option3() asynchronous as well, we eliminate most of the unexpected behavior – things, for the most part, happen in order*.  In fact, doing this drives home another point that Cleary made in an MDSN article titled "Best Practices in Asynchronous Programming": trying to mix synchronous and asynchronous code is tricky at best.  It’s better to make something asynchronous "all the way down".

The full solution for the sample used here can be found in the AsyncAndAwait.zip archive at http://Tinyurl.com/MarkGilbertSource.  You’ll need Visual Studio 2012 Update 3 to run it (I built it using the Express version).

 

 

* For the purposes of this demonstration, the “Press [Enter] to run again.” is executing out of order.  In my real program, the Main() routine didn’t have anything else to do other than wait for the steps to complete, so on the surface appeared a lot less weird.  Control was still being passed back to the Main() however, and I would have to be very careful about adding anything to my program.

Advertisements

July 1, 2013 - Posted by | Visual Studio/.NET

Sorry, the comment form is closed at this time.

%d bloggers like this: