Contents
Introduction
As developers we often strive to build concise code in order to avoid useless ceremony that would distract from its purpose.
We do that by leveraging the syntactic sugar offered by the languages to reduce code verbosity and increase its expressiveness but it can become a trap and lead to code that does not do what is expected.
In this article, I’ll illustrate this point with a powerful C# feature: async methods.
An async parsing method
Here is an async method that parses integers:
static async Task<int> ParseInt(string s) { Console.WriteLine($"Start parsing '{s}'."); await Task.Delay(1); Console.WriteLine($"End parsing '{s}'."); return int.Parse(s); }
As it is a “pure” function, calls are independent and can be executed in parallel.
A (too) concise code
And here is a naive usage of it:
static async Task TestParallelismKO() { int sum = await ParseInt("1") + await ParseInt("2") + await ParseInt("3"); Console.WriteLine($"Sum is {sum}"); }
The output is disappointing:
Start parsing '1'. End parsing '1'. Start parsing '2'. End parsing '2'. Start parsing '3'. End parsing '3'. Sum is 6
The result is correct but the calls are sequential!
More verbosity to understand
Here is a longer version that makes the root cause clear:
static async Task TestParallelismKO_Verbose() { var i1 = await ParseInt("1"); var i2 = await ParseInt("2"); var i3 = await ParseInt("3"); int sum = i1 + i2 + i3; Console.WriteLine($"Sum is {sum}"); }
So we are waiting for each parsing operation to be completed before jumping to the next.
The fix
Instead, we should:
1. First, run all the parsing tasks,
2. Then, wait for them to complete (whatever the order)
Here is an implementation of this:
static async Task TestParallelismOK() { var t1 = ParseInt("1"); var t2 = ParseInt("2"); var t3 = ParseInt("3"); int sum = await t1 + await t2 + await t3; Console.WriteLine($"Sum is {sum}"); }
And here is the output:
Start parsing '1'. Start parsing '2'. Start parsing '3'. End parsing '3'. End parsing '1'. End parsing '2'. Sum is 6
The calls are executed in parallel as expected.
Conclusion
Using syntactic sugar is great for readability and productivity but we must understand what is happening under the hood at the risk of writing incorrect code whose misbehaviour might be spotted later or even never.
Thank you for clarifying how to implement async/await in parallel context.
In your example, I would talk more about code factorisation than syntaxic sugar.
Thanks for your feedback.
You are right, code refactoring is playing a major role here too, syntactic sugar is more for the await operator, but I consider the fact we can combine the
await
s freely, like in arithmetic operations, as more syntactic sugar. 🙂Cool explanation sir. It gives me good clarity on async/await.
Thanks
My pleasure 🙂