Multitasking V, asynchronous programming with async and await
To conclude this series about programming multitasking applications, I will show the use of a simple mechanism that allows implementing asynchronous methods whose waiting times are used for the execution of other parallel tasks or events triggered by user interface controls.
If you want to start at the beginning, this is the first article in the series about multithreaded applications.
In this link you can download the MultithreadingDemo application source code with the examples, written in csharp with Visual Studio 2015. The application uses version 4.5 of the .NET Framework.
The mechanism we are going to talk about is based on the use of async, a modifier that allows that an event handler or method can make asynchronous calls, and the await operator, used to call a method asynchronously.
The await operator only can be used within a method declared as async. Methods called with this operator must return their result as a Task<data type> object, or Task if no results are returned. Thus, for example, if the method must return an integer, it must be declared with the return type Task<int>. If it is an event handler, you can use void instead of Task, in the usual way.
The call is made by placing the operator in front of the method, for example:
int i = await GetIntegerAsync();
As a convention, the name of these methods ends with the suffix Async, but this is not mandatory. What happens when we call a method this way is that the control automatically passes to another task, usually to the user interface one, until the method ends and the execution can continue with the next instruction.
This way, we can make our application continue to respond while performing operations that involve long waiting times, such as downloading a file or complex mathematical calculations, allowing the user to perform other tasks at the same time.
In the MultithreadingDemo application, you can select the Async / Await option from the Demo menu:
The first example simply draws a Feigenbaum diagram, a type of graphic used in the study of dynamic equations that takes a certain time and number of operations to calculate and draw. To launch it, just press the Feigenbaum button:
In order for the calculation of the diagram to be executed asynchronously, the Click event handler of the button must have the async modifier:
private async void bFeigenbaum_Click(object sender, EventArgs e)
{
try
{
bFeigenbaum.Enabled = false;
pbFeigenbaum.Image = null;
pbFeigenbaum.Image = await FeigenbaumAsync();
tabImages.SelectedIndex = 0;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
bFeigenbaum.Enabled = true;
}
}
The FeigenbaumAsync method is the one that draws the diagram, and is called using the await operator. It returns a Bitmap, so its return type must be Task<Bitmap>. The await operator extracts the Bitmap object and makes the returned value of this type:
private Task<Bitmap> FeigenbaumAsync()
{
return Task.Run(() =>
{
Bitmap bmp = new Bitmap(640, 480);
Graphics gr = Graphics.FromImage(bmp);
gr.FillRectangle(Brushes.White, new Rectangle(0, 0, 640, 480));
gr.Dispose();
for (float c = 3f; c <= 4f; c += 0.0015625f)
{
float x = 0.4f;
for (int i = 0; i < 600; i++)
{
x = c * x * (1 - x);
if (i >= 50)
{
bmp.SetPixel((int)((c - 3) * 640),
479 - (int)(x * 400), Color.Black);
}
}
}
return bmp;
});
}
You simply have to return a task that in turn returns an object of the declared type. When this task is finished, the control will return to the event handler, which will continue executing the next instruction, tabImages.SelectedIndex = 0;
, to show the result.
But this process runs too fast to notice some difference with the same operation executed synchronously. Therefore, we will implement another example that will be executed continuously within a loop, also adding a delay time, which will run asynchronously too.
We are going to draw random examples of the famous Pickover's biomorphs, a series of forms similar to microorganisms that are obtained by a recursive procedure using complex variable functions. To watch them, you have to press the Biomorph button:
Every second, a new biomorph is drawn in one of the four quadrants. However, the application continues to respond to the user actions, and we can re-launch the Feigenbaun diagram drawing without stopping the creature’s parade. The text of the button changes to Stop, to allow us to stop the process.
As in the previous example, we used the async modifier to convert the Click event handler into an asynchronous method:
private async void bBio_Click(object sender, EventArgs e)
{
try
{
if (bBio.Text == "Biomorph")
{
bBio.Text = "Stop";
_stopBio = false;
Bitmap bmp = new Bitmap(640, 480);
Graphics gr = Graphics.FromImage(bmp);
gr.FillRectangle(Brushes.White, new Rectangle(0, 0, 640, 480));
pbBio.Image = bmp.Clone() as Bitmap;
tabImages.SelectedIndex = 1;
int x = 0;
int y = 0;
Random rnd = new Random();
while (!_stopBio)
{
Bitmap bmpb = await BiomorphAsync(
new Complex(rnd.NextDouble() * (rnd.Next(3) + 1),
rnd.NextDouble() * (rnd.Next(3) + 1)),
rnd.Next(10),
rnd.Next(2));
gr.DrawImage(bmpb, x, y);
pbBio.Image = bmp.Clone() as Bitmap;
x = (x + 320) % 640;
if (x == 0)
{
y = (y + 240) % 480;
}
await WaitmsAsync(1000);
}
bBio.Text = "Biomorph";
}
else
{
_stopBio = true;
}
}
catch (Exception ex)
{
_stopBio = true;
MessageBox.Show(ex.Message);
bBio.Text = "Biomorph";
}
}
As you can see, it is possible to fire the same event even though it has not finished running at all, so, if you want to avoid this, you have to disable the button first, as in the rest of the examples.
The WaitmsAsync method simply waits for a given number of milliseconds, but only causes the delay in the code that has called it with await, so that time can be used by the rest of active processes.
But not all the asynchronous methods have to be implemented by you. Many of the .NET Framework classes, especially those related to input / output, have asynchronous methods that can be called using this mechanism. Let’s see an example of this using the File button in the demo. We will download asynchronously a file with an image, by using http, and we will write it, also asynchronously, in a file, to end showing it on the form. You can also perform this operation while any of the others is running:
The code does not have anything new, only that this time we call the await operator to methods of other classes:
private async void bFile_Click(object sender, EventArgs e)
{
FileStream fs = null;
try
{
bFile.Enabled = false;
pbFile.Image = null;
using (HttpClient client = new HttpClient())
{
Stream st = await client.GetStreamAsync("http://…/seashell.bmp");
fs = new FileStream("seashell.bmp", FileMode.Create,
FileAccess.ReadWrite, FileShare.None);
await st.CopyToAsync(fs);
await fs.FlushAsync();
fs.Seek(0, SeekOrigin.Begin);
pbFile.Image = Bitmap.FromStream(fs);
fs.Close();
fs = null;
tabImages.SelectedIndex = 2;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
if (fs != null)
{
fs.Close();
}
File.Delete("seashell.bmp");
bFile.Enabled = true;
}
}
A task executed with the await operator can in turn be asynchronous and make calls of this type, provided that it is declared using the async modifier. This modifier can also be used with lambda expressions, as in the next and last example that we are going to see.
In this example we will draw a mountain using a series of distorted triangles in a random way, pressing the Mountain button. The controller of the Click event is as in the previous examples, but this time, the task we launched is also declared as async, so it can call the WaitmsAsync method to produce a delay using await, so that the image takes an extra amount of time before presenting it:
private async Task<Bitmap> MountainAsync()
{
return await Task.Run(async () =>
{
Bitmap bmp = new Bitmap(640, 480);
Graphics gr = Graphics.FromImage(bmp);
gr.FillRectangle(Brushes.White,
new Rectangle(0, 0, 640, 480));
Random rnd = new Random();
int s = 150;
int g = 1;
int f = 0;
float l = 640f / s;
float h = (l / 1.4142f) - 0.6f;
float[,,] c = new float[151, 2, 2];
c[0, 0, 1] = 460f;
c[0, 0, 0] = 0f;
for (int k = 1; k <= s; k++)
{
c[k, 0, 0] = c[0, 0, 0] + l * k - 3 + rnd.Next(7);
c[k, 0, 1] = c[k - 1, 0, 1] - 3 + rnd.Next(7);
}
for (int j = 1; j <= s; j++)
{
for (int k = 0; k <= s - j; k++)
{
c[k, g, 0] = 3 - rnd.Next(7) + (c[k, f, 0] +
c[k + 1, f, 0]) / 2;
c[k, g, 1] = 3 - rnd.Next(7) - h +
(c[k, f, 1] + c[k + 1, f, 1]) / 2;
gr.DrawLine(Pens.DarkGray, c[k, f, 0], c[k, f, 1],
c[k + 1, f, 0], c[k + 1, f, 1]);
gr.DrawLine(Pens.Gray, c[k + 1, f, 0], c[k + 1, f, 1],
c[k, g, 0], c[k, g, 1]);
gr.DrawLine(Pens.Black, c[k, g, 0], c[k, g, 1],
c[k, f, 0], c[k, f, 1]);
}
f = 1 - f;
g = 1 - f;
await WaitmsAsync(30);
}
gr.Dispose();
return bmp;
});
}
You can see that the rest of the options continue working normally while the mountain is drawn.