Beware of Async Sub or Void by ThinqLinq

Beware of Async Sub or Void

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:

Hello World
Done Async
All Done

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:

Done Async
All Done
(2 second pause)
Hello World

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:

All Done
Hello World
Done Async

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.

Posted on - Comment
Categories: VB - VB Dev Center -
comments powered by Disqus