Eduasync part 10: CTP bug – don’t combine multiple awaits in one statement…

(This post covers project 12 in the source code.)

Last time, we saw what happens when we have multiple await expressions: we end up with multiple potential states in our state machine. That’s all fine, but there’s a known bug in the current CTP which affects the code generated when you have multiple awaits in the same statement. (Lucian Wischik calls these "nested awaits" although I personally don’t really think of one as being nested inside an another.)

This post is mostly a cautionary tale for those using the CTP and deploying it to production – but it’s also interesting to see the kind of thing that can go wrong. I’m sure this will all be fixed well before release, and the team are already very aware of it. (I expect it’s been fixed internally for a while. It’s not the kind of bug you’d hold off fixing.)

Just as a reminder, here’s the code we used last time to demonstrate multiple await expressions:

// Code from part 9
private static async Task<int> Sum3ValuesAsyncWithAssistance()
{
    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;
}

To simplify things a bit, let’s reduce it to two tasks. Then as a refactoring, I’m going to perform the awaiting within the summation expression:

private static async Task<int> Sum2ValuesAsyncWithAssistance()
{
    Task<int> task1 = Task.Factory.StartNew(() => 1);
    Task<int> task2 = Task.Factory.StartNew(() => 2); 

    return await task1 + await task2;
}

So, with the local variables value1 and value2 gone, we might expect that in the generated state machine, we’d have lost the corresponding fields. But should we, really?

Think what happens if task1 has completed (and we’ve fetched the results), but task2 hasn’t completed by the time we await it. So we call OnContinue and exit as normal, then keep going when the continuation is invoked.

At that point we should be ready to return… but we’ve got to use the value returned from task1. We need the result to be stored somewhere, and fields are basically all we’ve got.

Unfortunately, in the current async CTP we don’t have any of these – we just have two local awaiter result variables. Here’s the decompiled code, stripped of the outer skeleton as normal:

  int awaitResult1 = 0;
  int awaitResult2 = 0;
  switch (state)
  {
      case 1:
          break;

      case 2:
          goto Label_Awaiter2Continuation;

      default:
          if (state != -1)
          {
              task1 = Task.Factory.StartNew(() => 1);
              task2 = Task.Factory.StartNew(() => 2);

              awaiter1 = task1.GetAwaiter();
              if (awaiter1.IsCompleted)
              {
                  goto Label_GetAwaiter1Result;
              }
              state = 1;
              doFinallyBodies = false;
              awaiter1.OnCompleted(moveNextDelegate);
          }
          return;
  }
  state = 0;
Label_GetAwaiter1Result:
  awaitResult1 = awaiter1.GetResult();
  awaiter1 = default(TaskAwaiter<int>());

  awaiter2 = task2.GetAwaiter();
  if (awaiter2.IsCompleted)
  {
      goto Label_GetAwaiter2Result;
  }
  state = 2;
  doFinallyBodies = false;
  awaiter2.OnCompleted(moveNextDelegate);
  return;
Label_Awaiter2Continuation:
  state = 0;
Label_GetAwaiter2Result:
  awaitResult2 = awaiter2.GetResult();
  awaiter2 = default(TaskAwaiter<int>());

  result = awaitResult1 + awaitResult2;

The first half of the code looks like last time (with the natural adjustments for only having two tasks) – but look at what happens when we’ve fetched the result of task1. We fetch the result into awaitResult1, which is a local variable. We then don’t touch awaitResult1 again unless we reach the end of the code – which will not happen if awaiter2.IsCompleted returns false. When the method returns, the local variable’s value is lost forever… although it will be reset to 0 next time we re-enter, and nothing in the generated code will detect a problem.

So depending on the timing of the two tasks, the final result can be 3 (if the second task has finished by the time it’s checked), or 2 (if we need to add a continuation to task2 instead). This is easy to verify by forcing the tasks to sleep before they return.

Conclusion

The moral of this post is threefold:

  • In general, treat CTP and beta-quality code appropriately: I happened to run into this bug, but I’m sure there are others. This is in no way a criticism of the team. It’s just a natural part of developing a complex feature.
  • To avoid this specific bug, all you have to do is make sure that all relevant state is safely stored in local variables before any logical occurrence of "await".
  • If you’re ever writing a code generator which needs to store state, remember that that state can include expressions which have only been half evaluated so far.

For the moment, this is all I’ve got on the generated code. Next time, we’ll start looking at how exceptions are handled, and in particular how the CTP behaviour differs from the implementation we’ve seen so far. After a few posts on exceptions, I’ll start covering some daft and evil things I’ve done with async.

6 thoughts on “Eduasync part 10: CTP bug – don’t combine multiple awaits in one statement…”

  1. Woah, I hadn’t even considered trying that. I was treating await in much the same way as yield. But I guess since await lives on the right hand side of assignment, it makes sense that the syntax could be legal.

  2. I ran into this too, when writing a demo recently for a presentation on async. There’s that horrible moment of questioning your understanding before realizing that it’s a bug :)

  3. Even before I’ve read this article I was afraid from using the await anywhere but var result = await stuff, for reasons like this. Good to know the dangers were real :)

    On the other hand, I’ve really hit a dead stop while trying to write an app using the CTP. I seem to be getting a BadImageFormatException every time my code hits MoveNext(). (and no, I’m not running on a 64bit OS). If you could help me out it would greatly increase my chances of convincing my boss that the async-await is really good for something! (details are on SO: http://stackoverflow.com/questions/6283644/c5-asyncctp-badimageformatexception)

  4. @TDaver: Yikes. Will look into it. It’s probably just a codegen issue that the team would love to hear about.

  5. How about actually nested awaits (awaiting the result of an awaited task returning a task), do they work? Something like “await await taskReturningATask;”.

  6. @Trillian: I can’t say I’ve tried it – but I *expect* it would work, because it would set the hidden awaiter field.

Comments are closed.