What happens when you await? – The problem (part I)
This mini-series of three blog entries dives into what actually happens when you use the await keyword in C#. We will evolve a small code-base from a synchronous to a scaled-out asynchronous implementation, and get into the details as we go along. This is the first post and it presents the code base and illustrates the problems with it.
Async – Await
You have seen the samples. You have used it. You like it. But do you really understand how it works? These blog entries will try to dive down into details about this to help you get a better understanding of how it works, why it works that way and what pitfalls you might encounter. We might even throw a little ancient C code snippet at you . Let’s go.
The code base
In order to illustrate this, we have written a short (and perhaps somewhat contrived) sample. The sample tries to calculate pi eight times and visually indicates if it was successful or not. It is a Windows 8 app that on startup looks like this:
You press start, it will start calculating pi (it will do it eight times) and when it’s done it will hopefully look like this:
This indicates that it has calculated pi eight times, all were successful (hence the green color) and we can see that it took 6 seconds to run on my computer. The interesting parts of the code is here:
private void Start_Click(object sender, RoutedEventArgs e)
{
SetupRun();
RunScenario();
FinishRun();
}
private void RunScenario()
{
for (int i = 0; i < NumThreads; i++)
{
var pival = pi();
WorkDone(i, pival);
}
}
private double _pivalue;
public double pi()
{
double epsilon = 0.0000001;
double delta = 10;
int k = 1;
_pivalue = 0;
while (delta > epsilon)
{
_pivalue += Math.Pow(-1, (k + 1)) / (2 * k - 1);
delta = Math.Abs((4 * _pivalue) - realPi);
k++;
}
_pivalue = 4 * _pivalue;
return _pivalue;
}
private void WorkDone(int i, double pi)
{
SetRectColor(i, IsPiCorrect(pi));
}
So, Start_Click() is the event handler for the click or tap on the Start button, RunScenario() does the calculation with the the help of pi() and WorkDone() . These are the functions that we will change to add new functionality. Here are some additional helpers that is used for the update of the UI:
bool IsPiCorrect(double pi)
{
double diff = pi - realPi;
return diff < 0.0000001;
}
private void SetRectColor(int i, bool redorgreen)
{
var rect = (Rectangle)FindName(string.Format("r{0}", i));
rect.Fill = new SolidColorBrush(redorgreen ? Colors.Green : Colors.Red);
}
private void SetupRun()
{
for (int i = 0; i < NumThreads; i++)
{
var rect = (Rectangle)FindName(string.Format("r{0}", i));
rect.Fill = new SolidColorBrush(Colors.Yellow);
}
TimeStarted = DateTime.Now;
TimeElapsed.Text = "Started";
Timer.Start();
}
private void FinishRun()
{
Timer.Stop();
UpdateElapsedText();
}
void timer_Tick(object sender, object e)
{
UpdateElapsedText();
}
private void UpdateElapsedText()
{
TimeElapsed.Text = (DateTime.Now - TimeStarted).ToString("G");
}
I think these functions explains themselves and are not important in the context of this blog.
The problem
What is important though is how the application itself behaves. There are two fundamental problems:
- During calculation of pi, the application’s user interface locks up and is not responsive.
- Running it on a 8-core machine doesn’t make the program go any faster.
For a Windows Store app, these are severe problems. One of the main requirements of a Windows Store app is that the user interface should always be responsive, so that the user always feels in control. Utilizing multiple cores when you have something heavy to do might not be a Windows Store requirement, but it will delight your users when your app utilizes all of the hardware and delivers the result in a much short time.
The solution
The solution is to make proper use of asynchronous functions and to scale out the calculations on all available cores in the CPU. How to do that is explained in the coming blog posts: