Bienvenidos sean a este post, hoy veremos otra parte de las threads.
En el post anterior mencionamos un ejemplo de ejecucion de codigo asincronico, los eventos en la GUI de una aplicacion. Los componentes del GUI reaccionaran a las acciones de los usuarios disparando sus correspondientes eventos, y estos son agregados el queue de eventos. Estos son procesados invocando a las funciones manejadoras una por una. Esto se repite en un bucle y por eso se lo denomina habitualmente como event loop.
Los sistemas asincronicos son muy utiles en operaciones de E/S porque cualquier operacion de entrada o salida bloquea la ejecucion en el punto de llamada de E/S. Veamos el siguiente codigo:
auto f = read_file("archivo");
cout << "Bienvenido a la app!";
process_file_contents(f);
Ejemplo simple donde leemos el contenido de un archivo, una vez que se completo procede a mostrar el mensaje y recien llamamos a la funcion para poder procesar al contenido del archivo. Esto es un codigo sincronico pero cuando lidiamos con un codigo asincronico, todo lo que sabemos sobre la ejecucion del codigo comienza a comportarse de manera irreconocible. Tomemos el codigo anterior y hagamos el siguiente cambio:
auto f = read_file_async("archivo");
cout << "Bienvenido a la app!";
process_file_contents(f);
En este caso, lo convertimos a una lectura asincronica del archivo. Esto hara que el mensaje sea lo primero en mostrarse pero la ultima funcion no se podra ejecutar porque muy probablemente el archivo no haya sido procesado todavia. Esto es asi por la naturaleza de ejecucion asincronica nos permite ejecutar funciones por detras, la cual nos provee entrada/salida sin bloquear. Sin embargo, hay un pequeño cambio en como debemos tratar el valor de retorno de la funcion. Cuando trabajamos con funciones asincronicas, el valor devuelto se lo denomina como promesa, siendo esta la manera para que nos notifiquen que la funcion fue completada. Las promesas u objeto promesa pueden tener tres estados:
- Pending
- Rejected
- Fulfilled
Cuando una promesa toma el estado Fulfilled se considera como completa a la promesa, esto indica que la ejecucion fue exitosa. En cambio, si ocurrio algun error pasara al estado Rejected y cuando no tenga ninguno de los estados anteriores quedara en estado Pending.
En C++20 se introdujo corutinas como una adicion a las funciones asincronicas clasicas, moviendo las ejecuciones de fondo al proximo nivel porque ahora se nos permiten pausar o retomar una funcion cuando sea necesario. Vamos a suponer que leemos el contenido de un archivo, lo detenemos por la mitad, pasamos el contexto de ejecucion a otra funcion, y retomamos nuevamente la lectura del archivo hasta el final. Las funciones de corutina pueden ser como las siguientes:
- Started
- Paused
- Resumed
- Finished
Para hacer una funcion una corutina debes usar una de las siguientes palabras: co_await, co_yield, co_return. co_await es un constructo que le indica al codigo para esperar la ejecucion del codigo asincronico. Esto permite que el codigo puede ser suspendido en ese punto, y retomar la ejecucion cuando se haya obtenido el resultado. Veamos un ejemplo de esto:
task<void> process_image()
{
image i = co_await request_image("url");
// hacemos algo util con la imagen...
}
Este codigo es para la solicitud de una imagen, como esta solicitud corre sobre la red puede ser considerado como una operacion de entrada/salida y puede bloquear la ejecucion del codigo. Para prevenir este bloqueo, se utilizan las llamadas asincronicas. En el ejemplo anterior usamos un co_await y esto podria suspender la ejecucion de la funcion, y en esta funcion pueden pasar las siguientes cosas:
- Quita la funcion por un momento, mientras la informacion no esta lista
- Continua ejecutando desde donde fue llamado antes de process_image
- Vuelve para continuar ejecutando process_image desde el punto donde se dejo
Para lograr esto, una corutina no es manejada de la misma manera que las funciones regulares. Una de las caracteristicas mas interesantes y sorpresivas es que son stackless, no tienen stack, pero una funcion puede funcionar sin stack? A diferencia de una funcion ordinaria en lugar de mandar su informacion al stack, las corutinas la mandan al heap y desde ahi la recuperamos cuando retomamos.
Si miramos el codigo anterior, la funcion que llama a proccess_image transfiere la ejecucion a la corutina y la pausa, tambien conocido como yielding, para transferirr la ejecucion nuevamente al llamador. Si bien mencionamos que el estado de la corutina se almacena en el heap, toda la informacion como son los argumentos y variables locales se almacenan en el stack del llamador. Por lo tanto, la corutina esta asociada con un objeto que esta almacenado en el stack de la funcion llamadora., y su duracion esta relacionado con el objeto.
En resumen, hoy hemos visto una introduccion a las corutinas, que son, desde cuando se implementaron, algunos beneficios que nos brindan, y unos ejemplos para entender su concepto. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donation
It’s for maintenance of the site, thanks!
$1.50
