La mayor parte del trabajo preliminar para las corutinas ocurrió en los años 60/70 y luego se detuvo a favor de alternativas (por ejemplo, hilos)
¿Hay alguna sustancia en el renovado interés en las corutinas que ha estado ocurriendo en python y otros idiomas?
python
multithreading
concurrency
multitasking
usuario1787812
fuente
fuente
Respuestas:
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 con
yield
. 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.
fuente
Las corutinas solían ser útiles porque los sistemas operativos no realizaban una programación preventiva . Una vez que comenzaron a proporcionar una programación preventiva, ya no era necesario ceder el control periódicamente en su programa.
A medida que los procesadores multinúcleo se vuelven más frecuentes, se utilizan corutinas para lograr paralelismo de tareas y / o mantener alta la utilización de un sistema (cuando un hilo de ejecución debe esperar en un recurso, otro puede comenzar a ejecutarse en su lugar).
NodeJS es un caso especial, donde se utilizan corutinas para obtener acceso paralelo a IO. Es decir, se usan varios subprocesos para atender las solicitudes de E / S, pero se usa un solo subproceso para ejecutar el código javascript. El propósito de ejecutar un código de usuario en un hilo signle es evitar la necesidad de usar mutexes. Esto cae dentro de la categoría de tratar de mantener alta la utilización del sistema como se mencionó anteriormente.
fuente
Los primeros sistemas usaban corutinas para proporcionar concurrencia principalmente porque son la forma más simple de hacerlo. Los subprocesos requieren una buena cantidad de soporte del sistema operativo (puede implementarlos a nivel de usuario, pero necesitará alguna forma de organizar que el sistema interrumpa periódicamente su proceso) y son más difíciles de implementar incluso cuando tiene el soporte .
Los subprocesos comenzaron a hacerse cargo más adelante porque, en los años 70 u 80, todos los sistemas operativos serios los admitían (y, en los 90, ¡incluso Windows!), Y son más generales. Y son más fáciles de usar. De repente, todos pensaron que los hilos eran la próxima gran cosa.
A finales de los años 90, comenzaron a aparecer grietas, y a principios de la década de 2000 se hizo evidente que había serios problemas con los hilos:
Con el tiempo, la cantidad de tareas que los programas normalmente deben realizar en cualquier momento ha estado creciendo rápidamente, aumentando los problemas causados por (1) y (2) anteriores. La disparidad entre la velocidad del procesador y los tiempos de acceso a la memoria ha aumentado, exacerbando el problema (3). Y la complejidad de los programas en términos de cuántos y qué diferentes tipos de recursos requieren han ido creciendo, aumentando la relevancia del problema (4).
Pero al perder un poco de generalidad y poner un poco más de responsabilidad en el programador para que piense en cómo sus procesos pueden operar juntos, las rutinas pueden resolver todos estos problemas.
fuente
Prefacio
Quiero comenzar declarando una razón por la cual las corutinas no están resurgiendo, paralelizando. En general, las rutinas modernas no son un medio para lograr el paralelismo basado en tareas, ya que las implementaciones modernas no utilizan la funcionalidad de multiprocesamiento. Lo más parecido a eso son cosas como las fibras .
Uso moderno (por qué están de vuelta)
Las rutinas modernas se han convertido en una forma de lograr una evaluación diferida , algo muy útil en lenguajes funcionales como Haskell, donde, en lugar de iterar sobre un conjunto completo para realizar una operación, podrá realizar una evaluación de la operación solo lo necesario ( útil para conjuntos infinitos de elementos o conjuntos grandes con terminación temprana y subconjuntos).
Con el uso de la palabra clave Yield para crear generadores (que en sí mismos satisfacen parte de las necesidades de evaluación diferida) en lenguajes como Python y C #, en la implementación moderna no solo eran posibles, sino posibles sin una sintaxis especial en el lenguaje mismo. (aunque Python finalmente agregó algunos bits para ayudar). Las co-rutinas ayudan con la evacuación diferida con la idea de futuros s donde si no necesita el valor de una variable en ese momento, puede retrasar su adquisición hasta que solicite explícitamente ese valor (lo que le permite usar el valor y evaluarlo perezosamente en un momento diferente al de la instanciación).
Sin embargo, más allá de la evaluación perezosa, especialmente en la esfera web, estas co rutinas ayudan a solucionar el infierno de devolución de llamadas . Las rutinas se vuelven útiles en el acceso a la base de datos, las transacciones en línea, la interfaz de usuario, etc., donde el tiempo de procesamiento en la máquina del cliente no dará como resultado un acceso más rápido a lo que necesita. El enhebrado podría completar lo mismo, pero requiere mucha más sobrecarga en esta esfera, y en contraste con las rutinas, en realidad son útiles para el paralelismo de tareas .
En resumen, a medida que el desarrollo web crece y los paradigmas funcionales se fusionan más con los lenguajes imperativos, las rutinas se han convertido en una solución para los problemas asincrónicos y la evaluación perezosa. Las rutinas llegan a espacios problemáticos donde el enhebrado multiproceso y el enhebrado en general son innecesarios, inconvenientes o imposibles.
Ejemplo moderno
Las rutinas en lenguajes como Javascript, Lua, C # y Python derivan sus implementaciones mediante funciones individuales que dan el control del hilo principal a otras funciones (nada que ver con las llamadas al sistema operativo).
En este ejemplo de Python , tenemos una divertida función de Python con algo llamado
await
dentro de ella. Esto es básicamente un rendimiento, que produce ejecución alloop
que luego permite que se ejecute una función diferente (en este caso, unafactorial
función diferente ). Tenga en cuenta que cuando dice "Ejecución paralela de tareas" que es un nombre inapropiado, en realidad no se está ejecutando en paralelo, su ejecución de la función de entrelazado mediante el uso de la palabra clave esperar (lo que se debe tener en cuenta es solo un tipo especial de rendimiento)Permiten rendimientos de control únicos, no paralelos, para procesos concurrentes que no son tareas paralelas , en el sentido de que estas tareas no operan nunca al mismo tiempo. Las rutinas no son hilos en las implementaciones de lenguaje moderno. La implementación de todos estos lenguajes de las co-rutinas se derivan de estas llamadas de rendimiento de función (que el programador tiene que poner manualmente en sus rutinas co).
EDITAR: C ++ Boost coroutine2 funciona de la misma manera, y su explicación debería dar una mejor visión de lo que estoy hablando con yeilds, mira aquí . Como puede ver, no hay un "caso especial" con las implementaciones, cosas como las fibras de refuerzo son la excepción a la regla e incluso requieren una sincronización explícita.
EDIT2: dado que alguien pensó que estaba hablando del sistema basado en tareas C #, no lo estaba. Estaba hablando del sistema de Unity y las ingeniosas implementaciones de C #
fuente