Multitarea V, programación asíncrona con async y await
Para finalizar esta serie sobre la programación de aplicaciones multitarea, voy a mostrar el uso de un sencillo mecanismo que permite implementar métodos asíncronos que permiten ceder sus tiempos de espera para la ejecución de otras tareas paralelas o eventos disparados por controles del interfaz de usuario.
Si quieres empezar por el principio, este es el primer artículo de la serie sobre aplicaciones multitarea.
En este enlace puedes descargar el código fuente de la aplicación MultithreadingDemo con los ejemplos, escrita en csharp con Visual Studio 2015. La aplicación utiliza la versión 4.5 de .NET Framework.
El mecanismo del que vamos a hablar se basa en el uso de async, un modificador que posibilita que un método o controlador de eventos realice llamadas asíncronas, y el operador await, que permite llamar a un método de forma asíncrona.
Solo se puede usar el operador await dentro de un método declarado como async. Los métodos llamados con este operador deben devolver su resultado como un objeto Task<tipo de datos>, o bien Task si no devuelven ningún resultado. Así, por ejemplo, si el método debe devolver un entero, deberá estar declarado con el tipo de retorno Task<int>. Si se trata de un controlador de evento, se puede usar void en lugar de Task, de la manera habitual.
La llamada se realiza poniendo el operador delante del método, por ejemplo:
int i = await GetIntegerAsync();
De manera convencional, estos métodos llevan el sufijo Async en el nombre, pero esto no es obligatorio. Lo que ocurre cuando llamamos a un método de esta forma es que el control pasa automáticamente a otra tarea, normalmente la del interfaz de usuario, hasta que el método finaliza y el control continúa a partir la siguiente instrucción.
De esta manera, podemos hacer que nuestra aplicación continúe respondiendo mientras se realizan operaciones que conllevan tiempos de espera largos, como la descarga de un fichero o cálculos matemáticos complejos, permitiendo realizar al usuario otras tareas al mismo tiempo.
En la aplicación MultithreadingDemo, podemos seleccionar la opción Async / Await del menú Demo:
El primer ejemplo simplemente dibuja un diagrama de Feigenbaum, un tipo de diagrama utilizado en el estudio de ecuaciones dinámicas y que lleva un cierto tiempo y cantidad de operaciones calcular y dibujar. Para ejecutarlo, basta con pulsar el botón Feigenbaum:
Para que se pueda ejecutar de forma asíncrona el cálculo del diagrama, el controlador del evento Click del botón debe tener el modificador async:
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;
}
}
El método FeigenbaumAsync es el que dibuja el diagrama, y es llamado utilizando el operador await. Devuelve un Bitmap, por lo que su tipo debe ser Task<Bitmap>. El operador await extrae el objeto Bitmap y hace que el valor devuelto sea de este tipo:
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;
});
}
Simplemente hay que devolver una tarea que a su vez devuelva un objeto del tipo declarado. Cuando esta tarea termine, el control volverá al controlador del evento, que seguirá ejecutando la siguiente instrucción, tabImages.SelectedIndex = 0;
, para mostrar el resultado.
Pero este proceso se ejecuta demasiado rápido como para que podamos ver la diferencia con el funcionamiento síncrono de las aplicaciones. Por ello, vamos a implementar otro ejemplo que se ejecutará continuamente dentro de un bucle, añadiendo además un tiempo de retardo, que también se ejecutará de forma asíncrona.
Vamos a dibujar ejemplos aleatorios de los famosos biomorfos de Pickover, una serie de formas similares a microorganismos que se obtienen mediante un procedimiento recursivo utilizando funciones de variable compleja. Para ello, pulsaremos el botón Biomorph:
Cada segundo, se dibuja un biomorfo en uno de los cuatro cuadrantes. Sin embargo, la aplicación continua respondiendo al usuario, y podemos lanzar de nuevo el dibujo del diagrama de Feigenbaun sin que se detenga el desfile de criaturas. El texto del botón cambia a Stop, para que podamos detener el proceso.
Como en el ejemplo anterior, hemos utilizado el modificador async para convertir el controlador del evento Click en un método asíncrono:
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";
}
}
Como podéis ver, es posible volver a llamar al mismo evento aunque todavía no haya terminado de ejecutarse del todo, por lo que, si queréis evitar esto, es mejor deshabilitar el botón en primer lugar, como en el resto de ejemplos.
El método WaitmsAsync simplemente espera el número de milisegundos especificados, pero solo causa el retardo en el código que lo ha llamado con await, por lo que ese tiempo puede ser utilizado por el resto de procesos activos.
Pero no todos los métodos asíncronos los tenéis que implementar vosotros. Muchas de las clases del .NET Framework, sobre todo las que tiene que ver con la entrada / salida, tienen métodos asíncronos que se pueden llamar utilizando este mecanismo. Vamos a ver un ejemplo de esto mediante el botón File de la demo. Bajaremos de forma asíncrona un fichero con una imagen, usando http, y lo grabaremos, también de forma asíncrona, en un fichero, para mostrarlo al final en pantalla. Esta operación la podéis realizar también mientras se está ejecutando cualquiera de las demás:
El código no tiene nada nuevo, solo que esta vez llamamos con el operador await a métodos de otras clases:
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;
}
}
Una tarea ejecutada con el operador await puede a su vez ser asíncrona y realizar llamadas de este tipo, con tal de que sea declarada usando el modificador async. Este modificador también se puede usar con las expresiones lambda, como en el siguiente y último ejemplo que vamos a ver.
En este ejemplo dibujaremos una montaña utilizando una serie de triángulos deformados de forma aleatoria, con el botón Mountain. El controlador del evento Click es igual que en los casos anteriores, pero esta vez, la tarea que lanzamos la declaramos también como async, y llamará dentro de su código al método WaitmsAsync para producir un retardo, de modo que la imagen tarde un cierto tiempo en presentarse:
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;
});
}
Podéis ver que el resto de opciones siguen funcionando con normalidad mientras se dibuja la montaña.
Miguel, muchas gracias por los artículos. He entendido muchos conceptos al verlos en una aplicación funcionando y que no me era posible comprender en los ejemplos simples que veía por internet. Un abrazo.