Last time we had a really simple async method, and looked at the generated code. Even that took a little while… but in this post I’ll demonstrate some of the intricacies that get involved when the async method is more complex. In particular, this time we have:
- A parameter to the async method
- A "using" statement, so we’ll need a finally block
- Two loops
- The possibility of catching an exception generated from await and retrying
I’ve not made it as complicated as I could have done, mind you – we’ve still only got one await expression. Here’s the async method we’ll be investigating:
private static async Task<
int> WriteValuesAsyncWithAssistance(
int loopCount)
{
using (TextWriter writer = File.CreateText(
"output.txt"))
{
int sum = 0;
for (
int i = 0; i < loopCount; i++)
{
Task<
int> valueFetcher = Task.Factory.StartNew(() => 1);
for (int j = 0; j < 3; j++)
{
try
{
Console.WriteLine("Awaiting…");
int value = await valueFetcher;
writer.WriteLine("Got value {0}", value);
sum += value;
break;
}
catch (Exception)
{
Console.WriteLine("Oops… retrying");
}
}
}
return sum;
}
}
Okay, so it’s already reasonably complicated even as a normal method. But it gets nastier when we look at the code generated for it. In particular, this time the decompiled code actually isn’t quite valid C#. I’ll get to that in a minute. I’ve split the rest of this post into several smallish chunks to try to help keep several concerns separated. They’re not particularly pleasantly balanced in terms of size, segue etc… but hopefully they’ll make things simpler.
The decompiled code
I’m going to ignore the standard skeleton – basically everything you see here is within a try/catch block so that we can call SetException on the builder if necessary, and if we reach the end of the method normally, we’ll call SetResult on the builder using the "result" variable. If you want to see the complete method, it’s in the source repository. Here’s the cut down version:
bool doFinallyBodies =
true;
int tmpState = state;
if (tmpState != 1)
{
if (state == -1)
{
return;
}
this.writer = File.CreateText("output.txt");
}
try
{
tmpState = state;
if (tmpState == 1)
{
goto Label_ResumePoint;
}
sum = 0;
i = 0;
Label_ResumePoint:
while (i < loopCount)
{
if (state == 1)
{
goto Label_ResumePoint2;
}
valueFetcher = Task.Factory.StartNew(() => 1);
j = 0;
Label_ResumePoint2:
while (j < 3)
{
try
{
tmpState = state;
if (tmpState != 1)
{
Console.WriteLine("Awaiting…");
awaiter = valueFetcher.GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
doFinallyBodies = false;
awaiter.OnCompleted(moveNextDelegate);
return;
}
}
else
{
state = 0;
}
int awaitResult = awaiter.GetResult();
awaiter = new TaskAwaiter<int>();
value = awaitResult;
writer.WriteLine("Got value {0}", value);
sum += value;
break;
}
catch (Exception)
{
Console.WriteLine("Oops… retrying");
}
j++;
}
i++;
}
result = sum;
}
finally
{
if (doFinallyBodies && writer != null)
{
writer.Dispose();
}
}
Now that’s a fair bit of code… but if you were comfortable with the last post, there shouldn’t be much which is surprising. I’m not going to go over every aspect of it – just the surprising twists introduced by the extra complexities of the async method.
State and tmpState
Okay, let’s start from a point of ignorance: I don’t know why a local variable (tmpState) is introduced to take a copy of the state. My guess is that in some really complex scenarios it either becomes relevant when we want to change the state but also know what the state was, or if we’re testing the state several times without changing it, it may be quicker to do so having copied it to a local variable. I think we can pretty much ignore it – I’ve only included it here for completeness.
To do finally bodies or not to do finally bodies, that is the question
Last time the doFinallyBodies variable was pointless because we didn’t have any finally blocks – this time we do (albeit just the one). It’s implicit due to the "using" statement in the original code, but that’s really just a try/finally. The CreateText call occurs before we really enter into the using statement, which is why it’s before the try block in the decompiled code.
Broadly speaking, a try/finally block in an async method corresponds to the same try/finally block in the generated code, with one big difference: if we’re leaving the method because we’re just returning having attached a continuation to an awaiter, we don’t want to execute the finally body. After all, the method is effectively "paused" in the block rather than really exiting the block. So, any time you see an OnCompleted call, you’ll see a "doFinallyBodies = false;" statement… and that flag will be set to true at the start of the method, so that at any other time, exiting the try block will cause the finally body to be executed. We still want the body to execute if there’s an exception, for example. This is the same pattern used by the generated code for iterator blocks. (That’s extremely unsurprising at the moment, as the code generators share a lot of the code. It’s one bit which will have to be preserved if the two generators are split though.)
Go to where exactly?
As I mentioned earlier, we can’t write C# code which exactly represented the generated IL. That’s because when we enter the method in state 1 (i.e. for a continuation) the actual generated IL performs a goto straight into the start of the try block. You can’t do that in straight C#, which is why I’ve got an ugly double-goto in my representation. It doesn’t make it a 100% accurate representation of the IL, but it’s close enough for our purposes. If you build the async method and look at the generated code in Reflector, you’ll see that it basically shows a jump to the final comment above, just inside the try block. (Reflector actually shows the label as occurring before the try block, but the branch instruction in the IL really does refer to the first instruction in the try block.)
Now, you may be wondering why the generated code only jumps to the start of the try block. Why doesn’t it just jump straight to the "await" part? Last time it jumped straight to the "state = 0;" statement, after all… so we might expect it to do the same thing here.
It turns out that although IL has somewhat more freedom than C# in terms of goto (branch) statements, it’s not entirely free of restrictions. In this case, I believe it’s running up against section 12.4.2.8.2.7 of ECMA-335 (the CLI specification) which states:
If any target of the branch is within a protected block, except the first instruction of that protected block, the source shall be within the same protected block.
[…]
[Note: Code can branch to the first instruction of a protected block, but not into the middle of one. end note]
For "protected block" we can read "try block" (at least in this case).
I really shouldn’t enjoy the fact that even the C# compiler can’t do exactly what it would like to here, but somehow it makes me feel better about the way that the C# language restricts us. It’s possible that anthropomorphizing the C# compiler at all is a bad sign for my mental state, but never mind. It gets even worse if an await expression occurs in a nested try block – another "if" is required for each level involved.
The point is that by hook or by crook, when a continuation is invoked, we need to reach the assignment back to state 0, followed by the call to awaiter.GetResult().
Why do we go back to state 0?
You may have noticed that when we return from a continuation, we set the state to 0 before we call GetResult(). We only ever set to the state to a non-zero value just adding a continuation, or when we’ve finished (at which point we set it to -1). Therefore it will actually be 0 whenever we call GetResult() on an awaiter. But why? Shouldn’t state 0 mean "we’re just starting the method"? We know that state -1 means "the method has finished" and a positive state is used to indicate where we need to come back to when the continuation is called… so what sense does it make to reuse state 0?
This bit took me a while to work out. In fact, it had me so stumped that I asked a question on Stack Overflow about it. Various theories were presented, but none was entirely satisfactory. At the same time, I mailed Mads Torgersen and Lucian Wischik about it. After a while, Lucian got back to me and assured me that this wasn’t a bug – it was very deliberate, and absolutely required. He didn’t say why though – he challenged me to work it out, just in the knowledge that it was required. The example I eventually came up with was a slightly simpler version of the async method in this post.
The key is to think of states slightly differently:
- -1: finished
- 0: running normally; no special action required
- Anything else: Waiting to get back to a continuation point (or in the process of getting there)
So why must we go back to the "running normally" state as soon as we’ve reached the right point when running a continuation? Well, let’s imagine the flow in a situation like the async method shown, but where instead of just creating a trivial task, we’d started one which could fail, and does… but hasn’t finished before we first await it. Ignoring the outer loop and the using statement, we have something like this:
- The task is created
- We enter the inner loop
- We write out "Awaiting…" on the console
- We get an awaiter for the task, and check whether it’s completed: it hasn’t, so we add a continuation and return
- The task completes, so we call GetResult() on the awaiter
- GetResult() throws an exception, so we log it in the catch block
- We go back round the inner loop, and want to re-enter step 3
Now look at the flow control within the decompiled method again, and imagine if we hadn’t set the state to 0. We’d still be in state 1 when we reached the catch block, so when we got back to the top of the loop, we’d ignore the Console.WriteLine call and not even get a fresh awaiter – we’d end up reusing the same awaiter again. In other words, we’d try to get back to the continuation point.
To think of it another way: by the time we get back to the call to awaiter.GetResult(), the two possible paths (executing immediately vs adding a continuation, based on the value of awaiter.IsCompleted) have merged and we want to behave exactly the same way from that point onwards. So naturally we’d want to be in the same state… which is 0 for the synchronous path, as we haven’t had any reason to change state.
Conclusion
All this complexity, and just with one await expression! As is so often true in computing, it all seems a bit simpler when you can look past it being a wall of code, and break down each lump separately. Hopefully I’ve taken some of the surprises out of the generated code here, although please do leave a comment if anything is still unclear – I’ll try to update the post if so.
Next time we’ll look at an async method which is generally simpler, but which has three await expressions.