The Foreground Await / Async Anti-Pattern - Marcus Technical Services
Microsoft’s C# is an amazing language. It is a powerful, conceptually brilliant high-level tool for creating applications. One unfortunate side-effect of C#’s reputation is that anything published in the language API seems like some sort of visionary insight. That certainly was the case with the confusingly named Task Parallel Library and its accompanying design pattern, _ await / async _.
Some months back, I worked on an app that, when launched on Android devices, ran so slowly that it seemed “hung”. The IOS version did load, albeit slowly. The app was huge and poorly designed. However, if the coding had been the problem, both targeted operating systems would have failed – not just one. Something was slowing down Android in a way that IOS managed to survive, even if barely.
I searched the source code for _ awaits _. Twelve-hundred and fifty (yup: 1,250) of them showed up and mostly tied to the foreground thread. I identified “lynch-pin” locations, like setting up view models, _ removed _ the awaits, and replaced them with traditional _ threaded call-backs _. After that, the Android app was able to run. Under IOS, the app launched like a rocket.
On another recent project, a client complained that their Xamarin.Forms app drew jerky animations at load time.
Again, I searched out the _ awaits _ in the code. The code base was competently designed. Yet it contained _ 171 awaits _, most of them on (or tied directly to) the _ foreground thread _. I decided to settle the await/async foreground issue once and for all. I created a stripped-down test app that only contained: (1) the animations in question; (2) Task.Run style threaded call-backs; and (3) _ zero awaits _. Guess what? The animations flowed _ smoothly _.
This issue is so serious that it qualifies as an _ anti-pattern _: a thing that you should avoid doing.
What is Wrong with Async/Await on the Foreground Thread
If you had to select the best programmers from a random group of qualified candidates, you would be wise to focus on _ threading _. Threading possesses two deadly features: (1) It seems like a person could figure it out intuitively; and (2) Nothing in it occurs intuitively.
Microsoft understood this when they created the Task Parallel Library (TPL). They said to themselves, “amateur programmers are making too many threading mistakes. Let’s simplify it for them.” Which they did. At an extraordinary, hidden cost.
Ten years ago, if you wanted to read a file stream, you would create something like this:
private string ReadStream() { }
… however, these calls tended to bog down the foreground thread. So Microsoft developed TPL and started adding the Async suffix to most calls, both local and web-based:
private Task<string> ReadStreamAsync() { return Task.FromResult(string.Empty); }
To consume these new calls, the programmer calls await (which requires the term async
on the method signature):
private async Task<bool> DataExists() { var retStream = await ReadStreamAsync(); return (retStream.Length > 0); }
Any consumer of this method can be sure that while reading the stream, the app’s UI is “free” — the user can continue to tap UI elements and receive responses – because the activity does not occupy the foreground. The stream operates on a background thread.
There are some dilemmas with this new philosophy. The only way to consume an awaitable Task is _ asynchronously _, chaining into endless async methods. These async calls eventually “take over” the app through sheer numbers. They entangle the code like a clinging vine. The only way to end the chain is to jump off a “cliff” – a _ rooted _ startup location such as a constructor:
public class MyFirstClass { private bool _localDataExistsBool; public MyFirstClass() { // WARNING issued by Resharper and compiler: we are failing to await! VerifyDataExists(); } public async Task<bool> DataExists() { var retStream = await ReadStreamAsync(); return (retStream.Length > 0); } public async Task VerifyDataExists() { _localDataExistsBool = await DataExists(); } private string ReadStream() { return string.Empty; } private Task<string> ReadStreamAsync() { return Task.FromResult(string.Empty); } }
… yet that is _ illegal _ because a constructor is not async! So the code editor complains that the method VerifyDataExists
is not awaited and so will run asynchronously. After all of that trouble!
This gives rise to exotic techniques to call an async method legally from a non-async location:
public class MyFirstClass { // ...code as before public MyFirstClass() { try { Task.Run(async () => { await VerifyDataExists(); }); } catch { Debug.WriteLine("Error in Task.Run at constructor"); } } // ...code as before private void ConstructClass() { // Do some complex construction that relies on _localDataExistsBool } }