image.png

En estado mutable compartido contamos con N hilos que trabajan y modifican a un mismo estado mediante una referencia al mismo, mientras que en fork join dividimos tareas en sub-tareas hasta que estas sub-tareas son de un temaño el suficientemente pequeño como para poder resolverlas y posteriormente unir los resultados de estas para dar con el resultado de la tarea principal.

En fork join la resolución de la sub-tareas es independiente a las demás tareas y es por ello que podemos hacer uso de hilos para realizar por subtareas y además implementar work-stealing para maximizar los tiempos de resolución de las mismas.

En estado mutable compartido necesitamos que desde distintos hilos sea posible modificar un estado de forma en que cuando un hilo hace estas modificaciones no esté perjudicando a los demás, estas secciones se llaman secciones críticas y hacemos uso de locks para que solo uno de los hilos ejecute el código de la sección crítica y que no se presenten condiciones de carrera.

Los criterios de selección serían:

Para ejemplificar podemos pensar en un programa que procesa las palabras de archivos dado un directorio, la tarea es contar todas las palabras de todos los archivos bajo el mismo directorio y lo que podemos hacer es lanzar hilos para procesar los subdirectorios de tal directorio. Finalmente cada sub-tarea corresponde con procesar un archivo y si volvemos sobre la idea de work-stealing puede suceder que un hilo haya finalizado el procesamiento de los archivos de un directorio entero y por lo tanto continúe procesando los archivos de otro hilo para optimizar la performance de nuestro programa.

Para ejemplificar en el caso de estado mutable compartido podemos pensar en el servidor de un juego online donde múltiples jugadores (clientes) realizan modificaciones en el mapa de una partida determinada. Estos jugadores al querer modificar el mapa, solo podrán hacerlo si no hay otro jugador en la misma situación, aquí se representa esa sección crítica mencionada previamente.

image.png

En el modelo de programación asincrónica buscamos realizar tareas donde se aprovechen distintos puntos del código de modo que sea posible lanzar distintas tareas y avanzar en las mismas de manera concurrente gracias a estos puntos mencionados.

En el modelo tradicional de threads o procesos del sistema operativo contamos con ventajas como paralelizar disitintos cómputos CPU-intensive (lo cual depende de las capacidades de nuestro procesador en cuanto a la cantidad de hilos a ejecutar en paralelo) pero como desventajas como el overhead del uso de numerosos hilos debido al context-switch que lleva a cabo el scheduler.

Respecto a las tareas asíncronas las mismas se ejecutan dentro de un mismo hilo que es gestionado por un runtime o executor (si por ejemplo hacemos un sleep que no sea el que implementa el runtime vamos a terminar durmiendo el hilo en el que se ejecuta el runtime y por ende perjudicando la ejecución de todas las tareas que hayamos lanzado). En cuanto a ventajas tenemos un uso menor de memoria en comparación con los threads (para las tareas asíncronas no hay un context switch como con los hilos, el runtime administra el cambio de tareas a ejecutar el cual es mucho menor porque cada tarea no equivale a un hilo) y podemos lanzar numerosas tareas en contracara con los threads que se generación se ve limitada en función de aspectos de hardware de nuestro procesador.

En cuanto a ejemplos de uso, para programación asíncrona podemos tener una página web donde se hagan llamados a distintas APIs. Si el resultado de estos llamados no presenta una interdependencia y por ende podemos programar tareas asíncronas para cada uno de ellos, entonces podemos aprovechar esto para progresar en todas ellas de forma concurrente.

Por otro lado, en cuanto al uso de hilos del SO podemos pensar en la multiplicación numerosas matrices de forma que se lleven a cabo estas operaciones en distintos hilos y aprovechando entonces la capacidades de nuestra CPU.

image.png

Podríamos hacer uso de un Arc::new((Mutex::new(0), Condvar::new())) dado al incrementarse el valor detrás del mutex y el mismo ser equivalente al tamaño con el cual se inicializó la estructura es que entonces el valor se reinicia a cero y posteriormente se notifica a los demás hilos que se encuentren escuchando la condvar.

Con el uso indicado basta con que la condvar devuelva amount == size para que entonces el hilo avance reiniciando esa cantidad y notifique a los demás para que los mismos se despierten y sigan sumando a la misma (caso contrario, antes de ese bool amount == size se debe aproverchar el haber tomado el lock para entonces incrementar el contador).

image.png

En este código se observa que un hilo hijo ocupa un rol de productor al insertar en un buffer de capacidad 10 un u32 random siempre que haya espacio en el mismo, mientras tanto el hilo principal corrobora que el buffer no se encuentre vacío para quitar un elemento del mismo.