Here’s an interesting question I saw on StackOverflow recently; it was interesting because the answer seems obvious at first, but making a small change to the question makes the answer very different.
The original question was: suppose we have an asynchronous workflow where we need to get an integer to pass to another method. Which of these is, if any, is the better way to express that workflow?
Task<int> ftask = FAsync(); int f = await ftask; M(f);
int f = await FAsync(); M(f);
The answer of course is that all of these are the same workflow; they differ only in the verbosity of the code. You might argue that when debugging the code it is easier to debug if you have one operation per line. Or you might argue that efficient use vertical screen space is important for readability and so the last version is better. There’s not a clear best practice here, so do whatever you think works well for your application.
(If it is not clear to you that these are all the same workflow, remember that “await” does not magically make a synchronous operation into an asynchronous one, any more than “if(M())” makes M() a “conditional operation”. The await operator is just that: an operator that operates on values; the value returned by a method call is a value like any other! I’ll say more about the true meaning of await at the end of this episode.)
But now suppose we make a small change to the problem. What if instead we have:
M(await FAsync(), await GAsync());
? This workflow is equivalent to:
Task<int> ftask = FAsync(); int f = await ftask; Task<int> gtask = GAsync(); int g = await gtask; M(f, g);
but that causes the start of the GAsync task to be delayed until after the FAsync task finishes! If the execution of GAsync does not depend on the completion of FAsync then we would be better off writing:
Task<int> ftask = FAsync(); Task<int> gtask = GAsync(); int f = await ftask; int g = await gtask; M(f, g);
Task<int> ftask = FAsync(); Task<int> gtask = GAsync(); M(await ftask, await gtask);
and possibly get some additional efficiency in our workflow; if FAsync is for some reason delayed then we can still work on GAsync’s workflow.
Always remember when designing asynchronous workflows: an await is by definition a position in the workflow where the workflow pauses (asynchronously!) until a task completes. If it is possible to delay those pauses until later in the workflow, you can sometimes gain very real efficiencies!