At my VS Live last week, I gave a survey of Asynchronous programming from .Net 1 through .Net 4.5. As part of the presentation, I showed off this simple example of doing Async starting with a synchronous example. Here’s the beginning example:
Sub Main() DoWorkAsync() Debug.WriteLine("All Done")
Console.ReadLine() End Sub Sub DoWorkAsync() PrintIt() Debug.WriteLine("Done Async") End Sub Public Sub PrintIt() Dim text = "Hello World" Task.Delay(2000) Debug.WriteLine(text) End Sub
Before continuing on, try to figure out what you would see in the output window. For those impatient, here’s the output:
However, if you try this code, you will notice that the output is all produced prior to the expected 2 second delay (from the Task.Delay). Why is the delay ignored, because the TPL schedules the delay operation on a different thread than the main UI one and then lets the rest of the code operate on the main thread. We can force the delay to pause by changing the PrintIt method as follows:
Public Sub PrintIt() Dim text = "Hello World" Task.Delay(2000).Wait() Debug.WriteLine(text) End Sub
Now, we do delay for the 2 seconds as expected, but we loose any asynchronous behavior that we expected. As a result, we should use the new Async/Await keywords to make our PrintIt method async:
Public Async Sub PrintIt() Dim text = "Hello World" Await Task.Delay(2000) Debug.WriteLine(text) End Sub
Now, if we check our output, we’ll see the following results:
(2 second pause)
Notice here that the Done Async and All Done messages appear before Hello World. Why? Because when the line with the Await is encountered, control is passed back to the calling method (DoWorkAsync) and schedules the continuation of the await operation on the thread context of the Task.Delay operation. This is just one of the problems with the “Async Sub” or in C#, “async void” patterns. They are acceptable for fire and forget operations, but can cause issues if you want to rely on structured exception handling, resource disposal, and a number of other useful constructs, you shouldn’t use Async Sub. For more information, see Lucian Wischik’s Async Patterns article or any of a number of excellent articles by Stephen Toub.
So, how do we call the PrintIt operation asynchronously and ensure that it completes prior to continuing the “Done Async” operation? We need to change the signature of the PrintIt method to return a Task rather than returning nothing (void). We then need to move the await up the stack to also Await PrintIt and mark DoWorkAsync as Asynchronous.
Sub Main() DoWorkAsync() Debug.WriteLine("All Done") Console.ReadLine() End Sub Async Sub DoWorkAsync() Await PrintIt() Debug.WriteLine("Done Async") End Sub Public Async Function PrintIt() As Task Dim text = "Hello World" Await Task.Delay(2000) Debug.WriteLine(text) End Function
Now when we run our program, we see the following output:
Notice here that the “All Done” message appears before the 2 second delay, but Hello World and Done Async come out in the expected order showing how the DoWorkAsync operation was indeed run asynchronously from the rest of the app. If you want to see the internals of how the compiler interprets the Async and Await keywords, see my earlier post on using ILSpy with Async.
Also, if you are interested in using Async with Asp.Net or WCF, make sure to check out the Async session from Asp.Net Conf 2012 which details some of the potential issues you should consider there.