Potential pitfalls to avoid when passing around async lambdas

Stephen Toub - MSFT

One of the really useful capabilities of the new async methods feature in C# and Visual Basic is the ability to write async lambdas and anonymous methods (from here on in this post, I’ll refer to both of these as async lambdas, since the discussion applies equally to both).  This allows you to easily get a delegate to represent an asynchronous operation, e.g.

Func<Uri,Task<string>> getContentsLowerCaseAsync = async url =>
{
    string contents = await DownloadString(url);
    return contents.ToLower();
};

Async methods in C# and Visual Basic can return void, Task, or Task<TResult>, which means they can be mapped to delegates that return void, Task, or Task<TResult>.  This is very powerful, but it can also lead to subtle bugs if you’re not careful.

Most methods today that accept as a parameter a delegate that returns void (e.g. Action, Action<T>, etc.) expect the work of that delegate to be completed by the time the delegate completes.  As a simple example, consider a timing helper function, whose job it is to time how long a particular piece of code takes to execute:

public static double Time(Action action, int iters=10)
{
    var sw = Stopwatch.StartNew();
    for(int i=0; i<iters; i++) action();
    return sw.Elapsed.TotalSeconds / iters;
}

With this function, if I then run the following code:

static void Main()
{
    double secs = Time(() =>
    {
        Thread.Sleep(1000);
    });
    Console.WriteLine(“Seconds: {0:F7}”, secs);
}

I see this written out to the console:

Seconds: 0.9999956
Press any key to continue . . .

That’s what I’d expect: we asked to sleep for one second, and that’s almost exactly what the timing showed.  But now consider an alternate piece of code:

static void Main()
{
    double secs = Time(async () =>
    {
        await Task.Delay(1000);
    });
    Console.WriteLine(“Seconds: {0:F7}”, secs);
}

When I run this, I see the following written out to the console:

Seconds: 0.0000341
Press any key to continue . . .

Huh? Here we have an async method that’s awaiting a Task that won’t complete for a second, so this asynchronous method’s execution should also be at least a second, and yet the timer is telling us that it took only 34 microseconds?  What’s going on?

To understand this effect, we need to remember how async methods operate.  When you invoke an async method, it starts running synchronously.  If the method doesn’t have any awaits in it, or if all of the awaits in the method are on awaitables that are already completed by the time they’re awaited, then the method will run entirely synchronously.  However, when the method encounters the first await that yields, the async method returns.  In the case of an async method that returns a Task or a Task<TResult>, the method at this point returns the Task or Task<TResult> that represents the async method’s execution, and the caller can use that task to wait synchronous (e.g. Wait()) or asynchronously (e.g. await, ContinueWith) for the method to asynchronously complete.  In the case of a void method, though, no handle is handed back.  Async void methods are thus often referred to as “fire and forget.”

Now with that background, consider what’s happening with our timing function.  Our Time method accepts an Action, so the compiler is going to map our “async () => { … }” to being a void-returning async method, and the Action passed into the Time method will be for that void method.  Thus, when Time invokes the Action, the Action will return as soon as it hits the first await that yields, which is our await for the delay task.  This means that we’re really only timing the invocation of the async method up until the await, but not including the time to await the task or what comes after it.

We can fix this by modifying our Time function to accept a Func<Task> instead of an Action:

public static double Time(Func<Task> func, int iters=10)
{
    var sw = Stopwatch.StartNew();
    for (int i = 0; i < iters; i++) func().Wait();
    return sw.Elapsed.TotalSeconds / iters;
}

Now when I compile and run our async lambda, I get the following output that’s what I’d expect:

Seconds: 1.0078671
Press any key to continue . . .

Void-returning methods aren’t the only potentially problematic area; they’re just the easiest example to highlight, because it’s very clear from the signature that they don’t return anything and thus are only useful for their side-effects, which means that code invoking them typically needs them to run to completion before making forward progress (since it likely depends on those side-effects having taken place), and async void methods defy that.

A more complicated but still problematic example is a generic method that accepts an Action as a parameter and returns a Task, or that accepts a Func<…,TResult> as a parameter and returns a Task<TResult>, such as Task.Factory.StartNew.  Consider the following:

var t = Task.Factory.StartNew(() =>
{
    Thread.Sleep(1000);
    return 42;
});

Here StartNew accepts a delegate of type Func<int>, and returns a Task<int> representing the execution of the Func<int> delegate.  Makes sense.  But now consider the following:

var t = Task.Factory.StartNew(async () =>
{
    await Task.Delay(1000);
    return 42;
});

Any guesses as to what the type of ‘t’ is?  StartNew accepts a Func<TResult> and returns a Task<TResult>.  We’re passing in an async lambda that will give back a Task<int>, which means the TResult in Func<TResult> is actually Task<int>, such that the delegate provided to StartNew is a Func<Task<int>>.  That means that this call to StartNew is actually returning a Task<Task<int>>.  The task created by StartNew will invoke the Func<Task<int>>, which will run synchronously until the first await that yields, at which point the Func<Task<int>> will return, handing back the result Task<int> that represents the async lambda’s execution.  StartNew will then complete the Task<Task<int>> that it handed back, since the delegate associated with that task has completed its synchronous execution.  If I wrote code that depended on the returned task’s completion to mean that the async lambda had completed, I’d be sorely disappointed.  It’s actually the returned task’s Result (which is itself a Task<int>) that represents the async lambda.

There are a few ways to address this, such as using the Unwrap method: 

var t = Task.Factory.StartNew(async () =>
{
    await Task.Delay(1000);
    return 42;
}).Unwrap();

For more information, see my previous blog post on this (and on how Task.Run differs in behavior here from Task.Factory.StartNew) at https://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx.

0 comments

Discussion is closed.

Feedback usabilla icon