Last time we looked at a complex async method with nested loops and a single await. This post is the exact opposite – the method is going to look simple, but it will have three await expressions in. If you’re glancing down this post and feel put off by the amount of code, don’t worry – once you’ve got the hang of the pattern, it’s really pretty simple.
Here’s the async method we’re going to analyze:
{
Task<int> task1 = Task.Factory.StartNew(() => 1);
Task<int> task2 = Task.Factory.StartNew(() => 2);
Task<int> task3 = Task.Factory.StartNew(() => 3);
int value1 = await task1;
int value2 = await task2;
int value3 = await task3;
return value1 + value2 + value3;
}
Nice and straightforward: start three tasks, then sum their results. Before we look at the decompiled code, it’s worth noting that writing it this way allows the three (admittedly trivial tasks) to run in parallel. If we’d written it this way instead:
{
// No parallelism…
int value1 = await Task.Factory.StartNew(() => 1);
int value2 = await Task.Factory.StartNew(() => 2);
int value3 = await Task.Factory.StartNew(() => 3);
return value1 + value2 + value3;
}
… then we’d have waited for the first task to finish before starting the second one, then waited for the second one to complete before we started the third one. That’s appropriate when there are dependencies between your tasks (i.e. you need the result of the first as an input to the second) and it would still have been asynchronous but when you can start multiple independent tasks together, that’s generally what you want to do. Don’t forget that this doesn’t just extend to CPU-bound tasks – you might want to launch tasks making multiple web service calls in parallel, before collecting the results.
As before, I’ll just show the heart of the generated code, without its boiler-plate skeleton. Again, the full code is available online.
Generated code
{
case 1:
break;
case 2:
goto Label_Awaiter2Continuation;
case 3:
goto Label_Awaiter3Continuation;
default:
if (state != -1)
{
task1 = Task.Factory.StartNew(() => 1);
task2 = Task.Factory.StartNew(() => 2);
task3 = Task.Factory.StartNew(() => 3);
awaiter1 = task1.GetAwaiter();
if (awaiter1.IsCompleted)
{
goto Label_GetAwaiter1Result;
}
state = 1;
doFinallyBodies = false;
awaiter1.OnCompleted(moveNextDelegate);
}
return;
}
state = 0;
Label_GetAwaiter1Result:
int awaitResult1 = awaiter1.GetResult();
awaiter1 = new TaskAwaiter<int>();
value1 = awaitResult1;
awaiter2 = task2.GetAwaiter();
if (awaiter2.IsCompleted)
{
goto Label_GetAwaiter2Result;
}
state = 2;
doFinallyBodies = false;
awaiter2.OnCompleted(moveNextDelegate);
return;
Label_Awaiter2Continuation:
state = 0;
Label_GetAwaiter2Result:
int awaitResult2 = awaiter2.GetResult();
awaiter2 = new TaskAwaiter<int>();
value2 = awaitResult2;
awaiter3 = task3.GetAwaiter();
if (awaiter3.IsCompleted)
{
goto Label_GetAwaiter3Result;
}
state = 3;
doFinallyBodies = false;
awaiter3.OnCompleted(moveNextDelegate);
return;
Label_Awaiter3Continuation:
state = 0;
Label_GetAwaiter3Result:
int awaitResult3 = awaiter3.GetResult();
awaiter3 = new TaskAwaiter<int>();
value3 = awaitResult3;
result = value1 + value2 + value3;
Like last time, I’ll just go through the interesting points this raises, rather than examining it line by line.
Switch instead of if
In all our previous async methods, we’ve only had three possible states: -1 (finished), 0 (normal), 1 (return from continuation). The generated code always looked something like this to start with:
{
if (state == -1)
{
return;
}
// Execute code before the first await, loop or try
}
The comment is somewhat brief here, but the basic idea is that all the code which will only ever execute the first time (with no continuations) can go here. If there’s a loop that contains an await, then a continuation would have to jump back into that loop, so that code couldn’t be contained within this initial block. (A loop which didn’t have any awaits in could though.)
Anyway, this time we don’t have an "if" statement like that – we have a switch. It’s the same idea, but we could be in any of three different states when a continuation is called, depending on which await expression we’re at. The switch statement efficiently branches to the right place for a continuation, and executes the initial code otherwise. The "branch" for state 1 is just to exit the switch statement and continue from there.
It’s possible that the generated code actually has more levels of indirection than it needs; I don’t know about the details of what’s allowed within an IL switch, but it seems odd to effectively have an "On X goto Y" where Y immediately performs a "goto Z". If the switch statement could branch immediately to the right label, we’d end up with IL which probably wouldn’t have a hope of being decompiled to C#, but which might be slightly more efficient. It’s quite likely that the JIT can sort all of that out, of course.
I tend to actually think about all of this as if the code that’s really in the "default" case for the switch statement appeared after the switch, and the default case just contained a goto statement to jump to it. The effect would be exactly the same, of course – but it means I have a mental model of the method consisting of a "jump to the right place" phase before an "execute the code" phase. Just because I think of it that way doesn’t mean you have to, of course :)
Multiple awaiters and await results
There’s room for a bit more optimization in this specific case. We have three awaiters, but they’re all of the same type (TaskAwaiter<int>). Likewise, we have three await results, but they’re all int. (It would be possible to have different awaiter types with the same result type, of course.)
In the CTP (at least without optimization enabled) we end up with an awaiter / awaitResult pair of variables for each await expression. There’s never more than one await "active" at any one time, so the C# compiler could generate one awaiter variable per awaiter type, and one result variable per result type. In the common situation where the result is being directly assigned to a "local" variable of the same type within the method, we don’t really need the result variable at all. On the other hand, it’s only a local variable (unlike the awaiter) and it’s quite possible that the JIT can optimize this instead.
Ultimately it’s entirely reasonable for the C# compiler to be generating suboptimal code at this point in the development cycle. After all, it could be quite easy to introduce bugs due to inappropriate code generation… as we’ll see next time.
Conclusion
Other than the different way of getting to the right place on entry (using a switch instead of an if statement), async methods with multiple await expressions aren’t that hard to follow. Of course when you combine multiple awaits with loops, try/catch, try/finally blocks and any number of other things you might use to complicate the async method, things become tricky – but with the fundamentals covered in this blog series, hopefully you’d be able to cope with the generated code in any reasonable situation. Of course, it’s rare that you’ll need (or want) to look at the generated code in anything like the detail we have here – but now we’ve looked at it, you don’t need to wonder where the magic happens.
The next post will be the last one involving the decompiled code, at least for the moment. I’d like to demonstrate a bug in the CTP – mostly to show you how a small change to the async method can trigger the wrong results. I’m absolutely positive it will be fixed before release – probably for the next CTP or beta – but I think it’s interesting to see the sort of situation which can cause problems.
After that, we’re going to look at exception handling, before we move into a few odd way of using async – in particular, implementing coroutines and "COMEFROM".