April 16th, 2020

Why can’t you return an IAsync­Action from a coroutine that also does a co_await?

Suppose you’re writing a coroutine, and the last thing you do is call another coroutine which has exactly the same signature as your function. You might hope to be able to pull off some sort of tail call elimination.

IAsyncAction DoSomethingAsync(int value);

IAsyncAction MySomethingAsync(int value)
{
  auto adjusted_value = adjust_value(value);
  return DoSomethingAsync(adjusted_value);
}

If there are no co_await or co_return statements in your function, then it is not compiled as a coroutine, and you can just propagate the IAsync­Action as your own return value.

But if you use co_await or co_return, then your function becomes a coroutine, and propagation doesn’t work:

IAsyncAction MySomethingAsync(int value)
{
  auto adjusted_value = co_await AdjustValueAsync(value);
  return DoSomethingAsync(adjusted_value); // doesn't compile
}

Instead, you have to co_await the final coroutine.

IAsyncAction DoSomethingTwiceAsync(value)
{
  auto adjusted_value = co_await AdjustValueAsync(value);
  co_await DoSomethingAsync(adjusted_value);
}

Why can’t you just propagate the final coroutine as the return value of your own coroutine?

You can look at it in terms of the mechanics of co_await: The caller is going to co_await Do­Something­Twice­Async(), which means that they are going to obtain an awaiter for IAsync­Action and hook up their continuation to it. That awaiter is going to be managing the IAsync­Action that Do­Something­Twice­Async returns, which is not the same as the IAsync­Action that the inner Do­Something­Async returns.

Or you can look at it in terms of time travel: The transformation of Do­Something­Twice­Async into a coroutine causes the function to return an IAsync­Action at the point of the first suspension, whcih is at the co_await Adjust­Value­Async() call. When the function performs the co_await, it returns an IAsync­Action that represents the remainder of the coroutine. The code that calls Do­Something­Async hasn’t run yet, and consequently its IAsync­Action does not yet exist. When the coroutine resumes, it eventually gets around to calling Do­Something­Async and obtains an IAsync­Action. But it’s far too late to return that as the return value of Do­Something­Twice­Async; that function returned ages ago. You can’t go back in time and say, “Oops, sorry, that’s not the IAsync­Action I wanted to give you. Use this one instead.”

 

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

2 comments

Discussion is closed. Login to edit/delete existing comments.

  • Neil Rashbrook

    The good news is that as this is a compilation error, you won’t do something silly like this JavaScript example:

    async function async_foo() {
      try {
        return async_bar();
      } catch (ex) {
        // try to do something about ex
      }
    }

    Here the missing await makes a world of difference.

  • 紅樓鍮

    and aren't exactly equivalent even in the first example due to the different ways exceptions thrown by code executed before the gets handled. In Haskell's type system that would mean the difference between an and an . And also 's lifetime issue.

    Read more