Eduasync part 8: generated code from a complex async method

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; // Break out of the inner for loop
                }
                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;

// Effectively a three way switch: 1, -1, 0
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: // This shouldn’t quite be here… see below
    while (i < loopCount)
    {
        // Not in generated code:
        if (state == 1)
        {
            goto Label_ResumePoint2;
        }
        // Back to generated code

        valueFetcher = Task.Factory.StartNew(() => 1);
        j = 0;

        // Still not in the generated code, and still not quite right… we don’t want the j test here
      Label_ResumePoint2:
        // Back to generated code again…
        while (j < 3)
        {
            try
            {
                // We want Label_ResumePoint to be here really
                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:

  1. The task is created
  2. We enter the inner loop
  3. We write out "Awaiting…" on the console
  4. We get an awaiter for the task, and check whether it’s completed: it hasn’t, so we add a continuation and return
  5. The task completes, so we call GetResult() on the awaiter
  6. GetResult() throws an exception, so we log it in the catch block
  7. 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.

3 thoughts on “Eduasync part 8: generated code from a complex async method”

  1. Your example for state = 0 confused me for a bit, since the steps were for the async case, but the code suggests it’s the *sync* case that needs state = 0 when an exception occurs before the end of the protected block (not necessarily in GetResult()!).

    I find it strange to think about “jumping to above the first instruction, inside the try block” being any different to “jumping above the try block”, as far as I know*, there is no difference, “try {” is purely a syntactical marker for the beginning of the protected block, in the same way as “do {” doesn’t actually do anything except mark where to jump back to.

    * I beleive SEH involves pushing the protected ranges at the start of the function in x86-32, and static lookup tables for the ranges in x86-64 and Itanium.

    Like

  2. @Simon: No, it’s the async case which needs to explicitly assign state=0; in the sync case, the state will *already* be 0 without any extra assignments. But the state has to be the same by the time we actually call GetResult(), so that we continue in the same way afterwards.

    As for the try block – it’s “within” the try block in that the branch instruction points to an instruction which *is* included in the “try” range. I realize that it’s an invisible barrier, but I think it’s clearer to think of it as inside the try block. If you imagine an invisible nop between line of code, that nop could be before or after the try, but the branch would definitely point to an instruction *in* the try.

    I really only mentioned it because Reflector puts the label above the try block, whereas I’ve put it inside. The aim was to *avoid* confusion. Oops.

    Like

  3. OK, I think I got it the async/sync case now, when I was reading the generated code, I managed to dangle the “else { state = 0; }” on the last if, since I was still expecting state = 1 at the “if (state != 1)” on normal execution somehow :|.

    I’m not sure the try block thing really needs an explanation. Probably the “correct” way to think of it is that the instruction pointer (and therefore labels) is *before* instructions, not on them, so after a jump to the first instruction of a protected block, the IP is neither before nor after the start of the protected block, it’s *at* the start. In other words, “label: try {” and “try { label:” is just two ways to write the same logical thing, like “public static” and “static public”.

    Like

Leave a comment