I recently started using C# 5.0’s async/await extensively in a side project, while also testing our product’s support for the feature. Interactions between language features always provide plenty of opportunities for bugs. My test case was ADO.NET, whose public API in .NET 4.5 is full of asynchronous versions of existing methods. It was an interesting experience, so here are my observations:

  • At last, asynchronous code looks almost as straightforward as synchronous code. Asynchronous APIs finally look the way they should have from the beginning, without the machinery of BeginSmth()/EndSmth()/IAsyncResult, or the inverted control flow of the event-based asynchronous pattern, complete with CLR events that are difficult to compose.

    What still concerns me is how asynchronous code should coexist with the rest of the codebase. Monads and comonads can be awkward at their boundaries with ordinary code or other monads: somewhere, an asynchronous operation may need to be waited on synchronously, defeating the purpose of async, while there is no honest escape from the IO monad at all. In parts of an application that are naturally asynchronous, such as UI event handlers, this is often less of a problem: they can work on a fire-and-forget basis, with no caller needing to await the resulting tasks. Elsewhere, Task<T> starts spreading through return types, including methods that do no lengthy work themselves but call other methods already returning Task<T>.

  • As soon as cancellation is needed, explicitly passing and storing CancellationToken values everywhere starts to clutter the code. API designers also have to provide overloads with and without a token. F#’s Async<T> has a clear advantage here. For this purpose, a rough analogy is a task with an implicitly propagated cancellation token. An asynchronous computation can retrieve the current token and register code to run when cancellation is requested, without changing the caller’s code. You can start using cancellation when you need it, without first making sure a token has been threaded through every operation.

    The biggest problem is how easy it is to forget to pass the token in C# async code. I managed to do this repeatedly. Missing a token at one of several call sites often causes no obvious failure: the method just stops a little later. These omissions are difficult to find unless cancellation is tested regularly. My code ran in an infinite loop, polling web services, so I used cancellation every time I shut down the application. That was what helped me find the missing tokens quickly.

    The first draft of this post contained the sentence “Detecting omitted CancellationToken arguments would be a useful feature for an IDE tool.” Writing the post took long enough that I had time to implement it in our product:

    IDE inspection pointing out an overload with cancellation support

  • When a language allows constructs that serve little purpose, people will inevitably use them. In simple forwarding cases, for example, an await in tail position can be redundant, adding an otherwise empty continuation:

    private async Task CannotBeInCancelledState(CancellationToken token)
    {
      await Task.Run(
        cancellationToken: token,
        action: async () => { await smth; });
    }
    

    This is another useful inspection for an IDE tool, especially since I recently wrote a detector for tail positions in C#. I keep seeing these redundant tail-position await expressions in blog posts and code from colleagues, and I have written a few myself.

  • If you are designing an asynchronous API with cancellation support, think about what callers should expect when cancellation is requested. The SQL Server part of ADO.NET sometimes throws SqlException when an asynchronous operation is canceled, leaving the task in the Faulted state. I could not pin down exactly when this happens; perhaps it is during the early stages of sending a SQL query to the server.

    At other times, it throws the conceptually appropriate OperationCanceledException. This exception can carry the CancellationToken that caused the operation to stop up the stack, allowing the task associated with that token to recognize cancellation and transition to Canceled. That is how I would expect it to work, but ADO.NET does not always preserve the token. Consider this example; resources are deliberately left undisposed to keep it focused:

    var source = new CancellationTokenSource();
    var connection = new SqlConnection(connectionString);
    await connection.OpenAsync(source.Token);
    
    // create a command with an expensive query and execute it asynchronously
    var command = new SqlCommand("select count(*) from [?]", connection);
    var task = command.ExecuteScalarAsync(source.Token);
    
    Thread.Sleep(100); // wait briefly
    source.Cancel();   // request cancellation
    
    await task.ContinueWith(t =>
    {
      Console.WriteLine("Status: {0}", t.Status);
      try
      {
        Console.WriteLine("Result: {0}", t.Result);
      }
      catch (AggregateException exc)
      {
        var cancelled =
          exc.InnerExceptions.OfType<OperationCanceledException>().First();
    
        Console.WriteLine("== source.Token? {0}",
          cancelled.CancellationToken == source.Token);
        Console.WriteLine("== CancellationToken.None? {0}",
          cancelled.CancellationToken == CancellationToken.None);
      }
    });
    

    The result is:

    Status: Canceled
    == source.Token? False
    == CancellationToken.None? True
    

    Preserve the original token. Callers who compare cancellation tokens for equality will appreciate it.

  • I still find it unclear when an async method or a TPL API will expose an AggregateException, when it will not, and how much I need to worry about the distinction. In my project, any exception is simply logged and the operation restarted after a delay, so I have not investigated deeply. There is plenty of material online about exceptions in asynchronous code, but I would like to know what to expect without having to look it up.

  • Asynchronous lambdas and anonymous methods are very welcome; working without them would be painful. They do introduce an inconsistency, though: asynchronous code can be anonymous, while iterator code can only appear in methods. An iterator modifier, like the one in the latest VB.NET, could make anonymous iterators possible. That would introduce another inconsistency: existing iterator methods could omit the modifier for compatibility, while lambdas would need it. Eric Lippert has argued that anonymous methods and iterators are both complex transformations, making their combination difficult to justify. Given that lambdas can now be combined with the async transformation, I find that argument less convincing.

  • Avoid async methods that return void, even if you are sure nobody will care when the operation finishes. They exist primarily for compatibility with existing delegate types such as EventHandler and Action, which do not return Task. I suspect asynchronous lambdas were especially important here. If an async operation needs to be exposed through one of these delegate types, I would prefer a task-returning implementation with a separate void adapter. I have heard that there was considerable discussion about removing async void from the design before .NET 4.5 shipped, but it remained.

    There is also a testing trap: without a returned Task, a test framework may not know whether an asynchronous test has actually finished. Worse, it may report success as soon as the method returns at its first suspending await. Detecting this is another useful IDE inspection, planned for the next version of our product.

  • Looking at a loop such as while (await reader.ReadAsync()), it is natural to wonder about the overhead of an await on every iteration. A useful property of C# async is that, depending on the API, an entire async method can complete synchronously. In that case, its state machine does not need to be moved onto the heap. The compiler generates a value type for the state machine, which is boxed when necessary: at the first await that actually suspends execution.

    This is particularly relevant to the WinRT APIs used by the Metro side of Windows 8, where potentially lengthy operations are asynchronous and have no ordinary synchronous equivalents. When awaited operations complete synchronously, the performance difference can sometimes come down to just a few heap allocations.

    There is a trade-off. Unlike a yield return iterator, an async method is not entirely deferred: it starts executing synchronously and continues until an await actually suspends it. With the optimization described above, it can be hard to predict how long this synchronous portion will take. That matters when you call an async method without immediately awaiting it, intending to start work and await the result later. You can introduce an asynchronous boundary with await Task.Yield(); inside the method, or use Task.Run() at the call site. Immediate execution also has benefits: argument-validation code can run straight away, rather than being deferred as it is in an iterator, although exceptions from a task-returning async method are still recorded in that task.

    Another useful detail of async code generation is that reference fields in the state machine are cleared to null when they are no longer needed. This avoids a retention problem found in C# iterators: an iterator can keep references to locals even after reaching a state where those locals can never be used again. Enumerable iterators also retain the original parameter values in case GetEnumerator() is called again. Clearing dead references in async state machines lets the referenced objects be collected sooner. As far as I understand, the compiler omits these assignments to null when optimizations are disabled, so that the values remain available to the debugger.

  • Like C# 4.0’s dynamic, the async transformation is tightly tied to a small set of framework types, particularly System.Threading.Tasks.Task. We build our product with the C# 5.0 compiler, but cannot use async because it must run on .NET 3.5: we support Visual Studio versions going back to 2005. We also have our own threading infrastructure, including thread pools and dispatchers. I would like language features to depend less heavily on specific framework types.

  • My solution is only a few thousand lines long, so debugging async code has not been a problem for me. Colleagues using it in larger projects, however, often complain about losing stack traces and being unable to tell how execution reached a particular asynchronous call. This is a conceptual problem, but I think it ought to be solvable. Could stack traces be captured efficiently before each await, at least in debug builds? Otherwise, the Visual Studio debugging experience has been good, apart from stepping into code occasionally jumping around unpredictably at await expressions.