Tópicos Avanzados de Programación
Y es un tema muchas veces muy poco comprendido.
Ejecutar múltiples tareas juntas, compitiendo por los mismos recursos (principalmente CPU).
Ejecutar múltiples tareas en simultáneo, en el mismo instante.
El paralelismo es una forma de concurrencia, pero hay otras formas también.
Ej: "hacer un poquito de cada una" es concurrencia sin paralelismo.
Concurrencia? Paralelismo?
Concurrencia pero no paralelismo. Si pauso el tiempo, en cada instante estoy haciendo una cosa, no hay paralelismo.
Concurrencia? Paralelismo?
Ambos! Concurrencia con paralelismo real. Si pauso el tiempo, estaba haciendo ambas cosas en el mismo instante.
Antes de ir al código, tenemos que entender un par de cosas sobre ejecución de programas.
Correr varios procesos?
✅ Sí! Concurrencia, pero sin paralelismo real.
Correr varios hilos de un proceso?
✅ Sí! Concurrencia, pero sin paralelismo real.
Tenemos 3 técnicas para concurrencia y/o paralelismo:
Correr varios procesos, cada uno haciendo parte del trabajo.
Concurrencia? Paralelismo?
La memoria no es compartida...
Qué hacemos si necesitamos compartir info entre los procesos?
Correr varios hilos dentro de un mismo proceso.
Concurrencia? Paralelismo?
Igual que multiprocessing!
La memoria es compartida sin hacer nada extra!
Hasta ahora Multithreading y Multiprocessing suenan muy buenos. Pero...
Cambiar entre procesos o hilos es costoso! Hay que descargar/volver a cargar info de contexto en el procesador.
Esto empeora cuantos más hilos/procesos tengamos.
Si nos pausan y reanudan hilos/procesos en cualquier momento (cuando el OS decida), nuestra lógica se puede romper!
def actualizar_saldo(nuevo_pago):
saldo_actual = obtener_saldo_desde_la_db()
saldo_actual += nuevo_pago.monto
guardar_saldo_en_la_db(saldo_actual)
Qué pasa si nos pausan en la linea 3, otro hilo/proceso se ejecuta y cambia el saldo en la db, y luego nos vuelve a tocar a nosotros?
En un solo hilo de un solo proceso, correr muchas "corrutinas" livianas y colaborativas.
La clave: controlamos los context switches.
Este es por lejos el más difícil de entender...
Pero con código se puede visualizar mejor.
Repasando entonces:
Cambiar entre corrutinas es mucho más barato que cambiar entre hilos/procesos!
A veces a las corrutinas se les dice "hilos livianos".
Qué tan livianas?: hay servers async que responden 1 millón de requests por segundo.
Es muy difícil debuguear código cuando hay hilos/procesos independientes.
En async: 1 hilo, 1 proceso, si pausamos se pausa todo, podemos saltar de una corrutina a otra, etc. Mucho más fácil!
Peeeero...
Concurrencia? Paralelismo?
Es concurrencia, pero sin paralelismo.
La memoria es compartida sin hacer nada extra, como en Multithreading.
Pero además, es mucho más fácil evitar race conditions.
Cuál técnica es más rápida? Depende de la situación? De qué depende? Etc.
Si tenemos N cores, podemos combinar y ser super performantes:
Un proceso por core, usando async en cada proceso para sacarle el máximo de jugo a cada core.
Resolvamos un problema de ejemplo con las 3 técnicas y comparemos tiempos.