Las corutinas nunca se fueron, mientras que otras cosas las eclipsaron. El interés recientemente aumentado en la programación asincrónica y, por lo tanto, en las corutinas se debe en gran medida a tres factores: una mayor aceptación de las técnicas de programación funcional, conjuntos de herramientas con poco soporte para el verdadero paralelismo (¡JavaScript! ¡Python!) Y lo más importante: las diferentes compensaciones entre hilos y corutinas. Para algunos casos de uso, las corutinas son objetivamente mejores.
Uno de los paradigmas de programación más grandes de los años 80, 90 y hoy es OOP. Si observamos la historia de OOP y específicamente el desarrollo del lenguaje Simula, vemos que las clases evolucionaron a partir de las rutinas. Simula estaba destinado a la simulación de sistemas con eventos discretos. Cada elemento del sistema era un proceso separado que se ejecutaría en respuesta a eventos durante la duración de un paso de simulación, y luego permitiría que otros procesos hicieran su trabajo. Durante el desarrollo de Simula 67 se introdujo el concepto de clase. Ahora el estado persistente de la rutina se almacena en los miembros del objeto, y los eventos se desencadenan llamando a un método. Para más detalles, considere leer el documento El desarrollo de los idiomas SIMULA por Nygaard & Dahl.
Entonces, en un giro divertido, hemos estado usando corutinas todo el tiempo, solo los llamábamos objetos y programación dirigida por eventos.
Con respecto al paralelismo, hay dos tipos de lenguajes: los que tienen un modelo de memoria adecuado y los que no. Un modelo de memoria discute cosas como "Si escribo en una variable y luego leí esa variable en otro hilo, ¿veo el valor anterior o el nuevo valor o quizás un valor no válido? ¿Qué significa 'antes' y 'después'? ¿Qué operaciones están garantizadas para ser atómicas?
Crear un buen modelo de memoria es difícil, por lo que este esfuerzo simplemente nunca se ha hecho para la mayoría de estos lenguajes dinámicos de código abierto no especificados y definidos por la implementación: Perl, JavaScript, Python, Ruby, PHP. Por supuesto, todos esos lenguajes evolucionaron mucho más allá de los "scripts" para los que fueron creados originalmente. Bueno, algunos de estos lenguajes tienen algún tipo de documento de modelo de memoria, pero esos no son suficientes. En cambio, tenemos hacks:
Perl puede compilarse con soporte para subprocesos, pero cada subproceso contiene un clon separado del estado completo del intérprete, lo que hace que los subprocesos sean excesivamente caros. Como único beneficio, este enfoque de nada compartido evita las carreras de datos y obliga a los programadores a comunicarse solo a través de colas / señales / IPC. Perl no tiene una historia sólida para el procesamiento asíncrono.
JavaScript siempre ha tenido una gran compatibilidad con la programación funcional, por lo que los programadores codificarían manualmente las continuaciones / devoluciones de llamada en sus programas donde necesitaran operaciones asincrónicas. Por ejemplo, con solicitudes de Ajax o retrasos en la animación. Dado que la web es inherentemente asíncrona, hay mucho código asincrónico de JavaScript y administrar todas estas devoluciones de llamada es inmensamente doloroso. Por lo tanto, vemos muchos esfuerzos para organizar mejor esas devoluciones de llamada (Promesas) o para eliminarlas por completo.
Python tiene esta desafortunada característica llamada Global Interpreter Lock. Básicamente, el modelo de memoria de Python es “Todos los efectos aparecen secuencialmente porque no hay paralelismo. Solo un subproceso ejecutará código Python a la vez ”. Entonces, si bien Python tiene subprocesos, estos son simplemente tan poderosos como las rutinas. [1] Python puede codificar muchas corutinas a través de funciones generadoras conyield
. Si se usa correctamente, esto solo puede evitar la mayor parte del infierno de devolución de llamada conocido de JavaScript. El sistema asíncrono / espera más reciente de Python 3.5 hace que las expresiones idiomáticas asíncronas sean más convenientes en Python e integra un bucle de eventos.
[1]: Técnicamente, estas restricciones solo se aplican a CPython, la implementación de referencia de Python. Otras implementaciones como Jython ofrecen hilos reales que pueden ejecutarse en paralelo, pero tienen que pasar por un largo camino para implementar un comportamiento equivalente. Esencialmente: cada variable o miembro de objeto es una variable volátil , por lo que todos los cambios son atómicos y se ven inmediatamente en todos los hilos. Por supuesto, usar variables volátiles es mucho más costoso que usar variables normales.
No sé lo suficiente sobre Ruby y PHP para asarlos correctamente.
Para resumir: algunos de estos lenguajes tienen decisiones de diseño fundamentales que hacen que el subprocesamiento múltiple sea indeseable o imposible, lo que lleva a un enfoque más fuerte en alternativas como las rutinas y en formas de hacer que la programación asincrónica sea más conveniente.
Finalmente, hablemos sobre las diferencias entre corutinas e hilos:
Los subprocesos son básicamente como procesos, excepto que múltiples subprocesos dentro de un proceso comparten un espacio de memoria. Esto significa que los hilos no son "ligeros" en términos de memoria. Los hilos son programados de manera preventiva por el sistema operativo. Esto significa que los cambios de tareas tienen una alta sobrecarga y pueden ocurrir en momentos inconvenientes. Esta sobrecarga tiene dos componentes: el costo de suspender el estado del subproceso y el costo de cambiar entre el modo de usuario (para el subproceso) y el modo de núcleo (para el planificador).
Si un proceso programa sus propios subprocesos de manera directa y cooperativa, el cambio de contexto al modo kernel es innecesario y el cambio de tareas es comparablemente costoso a una llamada de función indirecta, como en: bastante barato. Estos hilos livianos pueden llamarse hilos verdes, fibras o corutinas dependiendo de varios detalles. Los usuarios notables de hilos / fibras verdes fueron las primeras implementaciones de Java, y más recientemente Goroutines en Golang. Una ventaja conceptual de las corutinas es que su ejecución puede entenderse en términos de flujo de control que pasa explícitamente de un lado a otro entre ellas. Sin embargo, estas rutinas no logran un verdadero paralelismo a menos que se programen en varios subprocesos del sistema operativo.
¿Dónde son útiles las corutinas baratas? La mayoría del software no necesita miles de millones de subprocesos, por lo que los subprocesos caros normales suelen estar bien. Sin embargo, la programación asíncrona a veces puede simplificar su código. Para ser utilizado libremente, esta abstracción tiene que ser lo suficientemente barata.
Y luego está la web. Como se mencionó anteriormente, la web es inherentemente asíncrona. Las solicitudes de red simplemente llevan mucho tiempo. Muchos servidores web mantienen un grupo de subprocesos lleno de subprocesos de trabajo. Sin embargo, la mayoría de las veces estos subprocesos estarán inactivos porque están esperando algún recurso, ya sea esperando un evento de E / S al cargar un archivo desde el disco, esperando hasta que el cliente haya reconocido parte de la respuesta o esperando hasta una base de datos consulta completa. NodeJS ha demostrado fenomenalmente que un diseño de servidor asíncrono y basado en eventos consecuente funciona extremadamente bien. Obviamente, JavaScript está lejos de ser el único lenguaje utilizado para aplicaciones web, por lo que también existe un gran incentivo para que otros lenguajes (que se notan en Python y C #) faciliten la programación web asincrónica.