Given that .NET 4.5 has only recently been released in its final form, it’s not surprising that many folks are still very new to the async/await keywords and have misconceptions about what they are and what they do (I’ve tried to clarify some of these in this Async/Await FAQ). One of the more common misconceptions is that a method marked as async forces asynchrony, and that’s not true: if there are no awaits in a method marked as async, or if none of the awaits in the async method ever result in suspensions (because they awaited awaitables that were already complete by the time the await occurred), then the method will run synchronously, meaning its code will run on the same thread as the caller and will run to completion before returning to its caller.
Still, there is an interesting boundary established when calling an async method. When an async method does suspend for the first time, it returns control flow to its caller, effectively forking the computation. When a call to a synchronous method, e.g. “Foo();”, is replaced by directly awaiting its asynchronous counterpart, e.g. “await FooAsync();”, this forking isn’t obvious, because the join with the forked operation occurs at the await point, immediately after the fork. If there’s no code between the time the fork occurs and the join occurs, from the perspective of this method, there is no concurrency (I say from the perspective of this method because the thread is being released to do other things during the await, so at a level higher than this method, there could still very well be concurrency). If, however, there is code between the fork and the join, the opportunity for concurrency from the perspective of this method exists:
var t = FooAsync();
… // code here that may run concurrently with the FooAsync operation
await t;
Not only can the concurrency be achieved between the calling method and the called method, but also between multiple called methods. The gap between the fork and the join can be used to fork other operations (one of the primary reasons Task.WhenAll exists, to support joining with multiple forked operations at the same time):
var t1 = FooAsync();
… // code
var t2 = BarAsync();
… // code
await Task.WhenAll(t1, t2);
Of course, this concurrency can only be achieved if the async operation actually forks something. As suggested at the beginning of this post, the async keyword is itself not enough to fork. For example, there is no forking in the following method:
async Task FooAsync()
{
}
…
FooAsync();
nor is there any here:
async Task FooAsync()
{
for(int i=0; i<100000; i++);
}
…
FooAsync();
nor is there any here:
async Task FooAsync()
{
for(int i=0; i<100000; i++)
await SomeMethodThatNeverSuspendsAsync();
}
…
FooAsync();
In all of these cases, the call to FooAsync() will start executing the code synchronously, and will finish executing the body of FooAsync synchronously. For a method implemented with async/await to result in the caller having the potential for concurrency, some await inside of the method needs to suspend, such that the async method doesn’t complete synchronously; the async method will at that point return to the caller, enabling the continuation that will continue running the method to run concurrently with the remainder of the caller.
Interestingly, await doesn’t enable a fork with respect to the body of the async method it’s in, only in regards to whatever method is calling the async method. This is because control flow from the perspective of any individual async method’s execution is serialized; when you await, the rest of that method won’t execute until the awaited awaitable completes, and thus you can’t have multiple awaits currently suspending in the same method at the same time.
To address this, some folks have suggested an additional language construct that would enable this kind of situation, an “async block”, e.g.
async Task FooAsync()
{
Console.WriteLine(“Starting FooAsync”);
Task t1 = async {
Console.WriteLine(“Starting first async block”);
await Task.Delay(1000);
Console.WriteLine(“Done first block”);
};
Task t2 = async {
Console.WriteLine(“Starting second async block”);
await Task.Delay(2000);
Console.WriteLine(“Done second block”);
};
await Task.WhenAll(t1, t2);
Console.WriteLine(“Done FooAsync”);
}
With this theoretical language feature, the expected output here would be something like:
Starting FooAsync
Starting first async block
Starting second async block
Done first block
Done second block
Done FooAsync
In other words, the first async block began executing synchronously but then yielded when it encountered the first await on a not-yet-completed awaitable (the delay task), at which point the block returned control to the caller, providing a task representing that block’s eventual completion. Then the second block began executing synchronously, and so on. This is exactly the behavior you’d expect if this had been rewritten with the blocks extracted into separate methods, e.g.
async Task FooAsync()
{
Console.WriteLine(“Starting FooAsync”);
Task t1 = Foo1Async();
Task t2 = Foo2Async();
await Task.WhenAll(t1, t2);
Console.WriteLine(“Done FooAsync”);
}async Task Foo1Async()
{
Console.WriteLine(“Starting first async block”);
await Task.Delay(1000);
Console.WriteLine(“Done first block”);
}async Task Foo2Async()
{
Console.WriteLine(“Starting second async block”);
await Task.Delay(2000);
Console.WriteLine(“Done second block”);
}
This mythical language construct is in fact mythical; it doesn’t exist. However, it doesn’t take much to achieve the same thing (beyond separating out the code into separate named methods, which you can of course do). In Visual Basic, you can get very close to this, due to VB’s forgiving nature when it comes to using lambdas and automatically inferring an appropriate type for them even when no delegate type is ever mentioned, e.g.
Async Function FooAsync() As Task
Console.WriteLine(“Starting FooAsync”)
Dim t1 = (Async Function()
Console.WriteLine(“Starting first async block”)
Await Task.Delay(1000)
Console.WriteLine(“Done first block”)
End Function)()
Dim t2 = (Async Function()
Console.WriteLine(“Starting second async block”)
Await Task.Delay(2000)
Console.WriteLine(“Done second block”)
End Function)()
Await Task.WhenAll(t1, t2)
Console.WriteLine(“Done FooAsync”)
End Function
Here I’m able to declare the asynchronous lambda and then immediately invoke it, providing a syntax extremely close to that of the mythical async block feature. In C#, the same thing can be done, but we need to declare the type for the delegate, making the syntax a bit more cumbersome, e.g.
async Task FooAsync()
{
Console.WriteLine(“Starting FooAsync”);
Task t1 = ((Func<Task>)(async delegate
{
Console.WriteLine(“Starting first async block”);
await Task.Delay(1000);
Console.WriteLine(“Done first block”);
}))();
Task t2 = ((Func<Task>)(async delegate
{
Console.WriteLine(“Starting second async block”);
await Task.Delay(2000);
Console.WriteLine(“Done second block”);
}))();
await Task.WhenAll(t1, t2);
Console.WriteLine(“Done FooAsync”);
}
For cases where I want to use this sort of async block-like construct, I prefer to use a few very simple helper methods:
static void AsyncBlockVoid(Action asyncMethod) { asyncMethod(); }
static Task AsyncBlock(Func<Task> asyncMethod) { return asyncMethod(); }
static Task<T> AsyncBlock<T>(Func<Task<T>> asyncMethod) { return asyncMethod(); }
Each of these methods just accepts a delegate as a parameter and invokes that delegate, but in doing so it provides to the C# compiler enough information for it to infer an appropriate delegate type for the async delegate, e.g.
async Task FooAsync()
{
Console.WriteLine(“Starting FooAsync”);
Task t1 = AsyncBlock(async delegate {
Console.WriteLine(“Starting first async block”);
await Task.Delay(1000);
Console.WriteLine(“Done first block”);
});
Task t2 = AsyncBlock(async delegate {
Console.WriteLine(“Starting second async block”);
await Task.Delay(2000);
Console.WriteLine(“Done second block”);
});
await Task.WhenAll(t1, t2);
Console.WriteLine(“Done FooAsync”);
}
This gives me a clean and understandable way of achieving the same thing, and without a special language feature. It also maps nicely to other methods that take a delegate, return a task, but add additional behaviors. For example, I previously described how these async block constructs resulted in the delegate being invoked synchronously. What if I wanted it to be invoked asynchronously? I can just change “AsyncBlock” above to “Task.Run”, and I get the same behavior except with the async delegate being invoked asynchronously rather than synchronously, e.g.
async Task FooAsync()
{
Console.WriteLine(“Starting FooAsync”);
Task t1 = Task.Run(async delegate {
Console.WriteLine(“Starting first async block”);
await Task.Delay(1000);
Console.WriteLine(“Done first block”);
});
Task t2 = Task.Run(async delegate {
Console.WriteLine(“Starting second async block”);
await Task.Delay(2000);
Console.WriteLine(“Done second block”);
});
await Task.WhenAll(t1, t2);
Console.WriteLine(“Done FooAsync”);
}
Enjoy.
0 comments