The curious case of async, await, and IDisposable

Consider this two methods:

 

public Task DoWorkAsync()
{
    var arg1 = ComputeArg();
    var arg2 = ComputeArg();
    return AwaitableMethodAsync(arg1, arg2);
}

public async Task DoWork2Async()
{
    var arg1 = ComputeArg();
    var arg2 = ComputeArg();
    await AwaitableMethodAsync(arg1, arg2);
}

 

Do you notice the difference?

The first is a synchronous method that returns a Task. The Task may or may not have completed when the method returns. The second as an async method that returns the result of awaiting other work.

These two methods look almost the same, but the code generated by the compiler for them is very different. These two talks on InfoQ by Jon Skeet and I go into all the gory details about the differences:

In most cases, you should prefer writing the first version when possible. The method is much simpler, and is much easier to reason about. It’s a synchronous method that returns an object that represents work that may be ongoing.

The second is more complicated. It builds a state machine. It manages re-entrancy for code that should execute when the awaited task finishes. It returns. It resumes execution. It’s difficult to reason about.

You can write the first version for any task-returning method that could be a synchronous method. That’s the case when:

  • The method does not do any work after the only task-returning method is called.
  • The return of the only task returning method matches the signature of this method.

The curious case of IDisposable

Now, let’s look at a variation of the two methods above:

 

public Task DoWorkAsync()
{
    using (var service = new Service())
    {
        var arg1 = ComputeArg();
        var arg2 = ComputeArg();
        return service.AwaitableMethodAsync(arg1, arg2);
    }
}

public async Task DoWork2Async()
{
    using (var service = new Service())
    {
        var arg1 = ComputeArg();
        var arg2 = ComputeArg();
        await service.AwaitableMethodAsync(arg1, arg2);
    }
}

 

Can you spot the difference? Can you spot the bug? The introduction of a local variable the refers to an object the implements IDisposable means you must use the second version, where the compiler generates the state machine and a continuation.

I gave a hint as to the reason in the first description. The first method is synchronous. There are no continuations. The service object will be Disposed() as soon as AwaitableMethodAsync() returns. The object is disposed if the async work is completed. The object is disposed when the async work is not completd. The compiler generated finally clause will be executed before the method returns the (possibly still running) task. There is a high-probability that this idiom results in an ObjectDisposedException in some cases.

The asynchronous method generates the code so that the compiler generated finally clause executes only after the task returned from AwaitableMethodAsync() completes. The service will be Disposed only when it’s done doing all its work.

Note that my explanation of when you can write the synchronous version above is accurate: because of the compiler generated finally clause, there is code that must execute after the task completes. It’s just not easily visible in your source code.

Testing for this case

This condition can be hard to catch in automated unit tests. (In fact the error I introduced this week was not caught by unit tests in the library I was working on.) Often we write unit tests for asynchronous methods that always return synchronously, using Task.FromResult(). These tests are fine, and verify that the fast path works correctly.

You should also write tests that verify the slow path, where a Task has not completed synchronously. It doesn’t have to be measurably slow. Just sprinkle an ‘await Task.Yield()’ statement in your mock implementation and you will force the slow path.

Yes, that bug I introduced is fixed. It’s also now caught by a test.

Created: 5/3/2017 8:37:36 PM

Current Projects

I create content for .NET Core. My work appears in the .NET Core documentation site. I'm primarily responsible for the section that will help you learn C#.

All of these projects are Open Source (using the Creative Commons license for content, and the MIT license for code). If you would like to contribute, visit our GitHub Repository. Or, if you have questions, comments, or ideas for improvement, please create an issue for us.

I'm also the president of Humanitarian Toolbox. We build Open Source software that supports Humanitarian Disaster Relief efforts. We'd appreciate any help you can give to our projects. Look at our GitHub home page to see a list of our current projects. See what interests you, and dive in.

Or, if you have a group of volunteers, talk to us about hosting a codeathon event.