¿Qué son las corrutinas en C ++ 20?


104

¿Qué son las corrutinas en ?

¿En qué se diferencia de "Parallelism2" o "Concurrency2" (mire la imagen de abajo)?

La siguiente imagen es de ISOCPP.

https://isocpp.org/files/img/wg21-timeline-2017-03.png

ingrese la descripción de la imagen aquí


3
Para responder "¿En qué se diferencia el concepto de corrutinas del paralelismo y la concurrencia ?" - en.wikipedia.org/wiki/Coroutine
Ben Voigt


3
Una introducción muy buena y fácil de seguir a las corrutinas es la presentación de James McNellis "Introducción a las corrutinas C ++" (Cppcon2016).
philsumuru

2
Finalmente, también sería bueno cubrir "¿En qué se diferencian las corrutinas en C ++ de las implementaciones de corrutinas y funciones reanudables de otros lenguajes?" (que el artículo de wikipedia vinculado anteriormente, al ser independiente del idioma, no aborda)
Ben Voigt

1
¿Quién más leyó esta "cuarentena en C ++ 20"?
Sahib Yar

Respuestas:


199

En un nivel abstracto, Coroutines separó la idea de tener un estado de ejecución de la idea de tener un hilo de ejecución.

SIMD (instrucción única de múltiples datos) tiene múltiples "subprocesos de ejecución" pero solo un estado de ejecución (solo funciona con múltiples datos). Podría decirse que los algoritmos paralelos son un poco así, en el sentido de que tiene un "programa" que se ejecuta en diferentes datos.

El subproceso tiene varios "subprocesos de ejecución" y varios estados de ejecución. Tiene más de un programa y más de un hilo de ejecución.

Coroutines tiene varios estados de ejecución, pero no posee un hilo de ejecución. Tiene un programa y el programa tiene estado, pero no tiene hilo de ejecución.


El ejemplo más sencillo de corrutinas son los generadores o enumerables de otros lenguajes.

En pseudocódigo:

function Generator() {
  for (i = 0 to 100)
    produce i
}

Se Generatorllama y la primera vez que se llama vuelve 0. Se recuerda su estado (cuánto varía el estado con la implementación de las corrutinas) y la próxima vez que lo llame continúa donde lo dejó. Entonces devuelve 1 la próxima vez. Entonces 2.

Finalmente llega al final del ciclo y cae al final de la función; la corrutina está terminada. (Lo que sucede aquí varía según el lenguaje del que estamos hablando; en Python, arroja una excepción).

Las corrutinas llevan esta capacidad a C ++.

Hay dos tipos de corrutinas; apilables y sin apilar.

Una corrutina sin pila solo almacena variables locales en su estado y ubicación de ejecución.

Una corrutina apilada almacena una pila completa (como un hilo).

Las corrutinas sin apilamiento pueden ser extremadamente ligeras. La última propuesta que leí involucraba básicamente reescribir su función en algo un poco como una lambda; todas las variables locales pasan al estado de un objeto, y las etiquetas se utilizan para saltar hacia / desde la ubicación donde la corrutina "produce" resultados intermedios.

El proceso de producir un valor se llama "rendimiento", ya que las corrutinas son un poco como multiproceso cooperativo; está cediendo el punto de ejecución a la persona que llama.

Boost tiene una implementación de corrutinas apiladas; le permite llamar a una función para ceder por usted. Las corrutinas apiladas son más potentes, pero también más caras.


Las corrutinas son más que un simple generador. Puede esperar una corrutina en una corrutina, lo que le permite componer corrutinas de una manera útil.

Las corrutinas, como if, bucles y llamadas a funciones, son otro tipo de "goto estructurado" que le permite expresar ciertos patrones útiles (como máquinas de estado) de una manera más natural.


La implementación específica de Coroutines en C ++ es un poco interesante.

En su nivel más básico, agrega algunas palabras clave a C ++:, co_return co_await co_yieldjunto con algunos tipos de bibliotecas que funcionan con ellas.

Una función se convierte en una corrutina al tener una de esas en su cuerpo. Entonces, de su declaración, son indistinguibles de las funciones.

Cuando se utiliza una de esas tres palabras clave en el cuerpo de una función, se produce un examen obligatorio estándar del tipo de retorno y los argumentos y la función se transforma en una corrutina. Este examen le dice al compilador dónde almacenar el estado de la función cuando la función está suspendida.

La corrutina más simple es un generador:

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yieldsuspende la ejecución de funciones, almacena ese estado en generator<int>, luego devuelve el valor de a currenttravés de generator<int>.

Puede recorrer los enteros devueltos.

co_awaitmientras tanto, le permite unir una corrutina con otra. Si está en una corrutina y necesita los resultados de algo que se espera (a menudo una corrutina) antes de progresar, hágalo co_await. Si están listos, proceda de inmediato; si no, suspendes hasta que esté listo el awaitable que estás esperando.

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}

load_dataes una corrutina que genera un std::futurecuando se abre el recurso nombrado y logramos analizar hasta el punto donde encontramos los datos solicitados.

open_resourcey read_lines son probablemente corrutinas asincrónicas que abren un archivo y leen líneas de él. El co_awaitconecta el estado de suspensión y listo de load_datacon su progreso.

Las corrutinas de C ++ son mucho más flexibles que esto, ya que se implementaron como un conjunto mínimo de características de lenguaje además de los tipos de espacio de usuario. Los tipos de espacio de usuario definen efectivamente qué co_return co_awaity co_yield significan : he visto a personas usarlo para implementar expresiones opcionales monádicas, de modo que un co_awaiten un opcional vacío automáticamente propaga el estado vacío al opcional externo:

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  return (co_await a) + (co_await b);
}

en vez de

std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}

26
Esta es una de las explicaciones más claras de lo que son las corrutinas que he leído. Compararlos y distinguirlos de SIMD y los hilos clásicos fue una idea excelente.
Omnifarious

2
No entiendo el ejemplo de complementos opcionales. std :: opcional <int> no es un objeto esperado.
Jive Dadson

1
@mord sí, se supone que devuelve 1 elemento. Puede que necesite pulir; si queremos más de una línea necesitamos un flujo de control diferente.
Yakk - Adam Nevraumont

1
@lf lo siento, se suponía que era ;;.
Yakk - Adam Nevraumont

1
@LF para una función tan simple, tal vez no haya diferencia. Pero la diferencia que veo en general es que una corrutina recuerda el punto de entrada / salida (ejecución) en su cuerpo, mientras que una función estática comienza la ejecución desde el principio cada vez. La ubicación de los datos "locales" es irrelevante, supongo.
avp

21

Una corrutina es como una función C que tiene múltiples declaraciones de retorno y cuando se llama por segunda vez no inicia la ejecución al comienzo de la función sino en la primera instrucción después del retorno ejecutado anteriormente. Esta ubicación de ejecución se guarda junto con todas las variables automáticas que vivirían en la pila en funciones que no sean de rutina.

Una implementación de corrutina experimental anterior de Microsoft usó pilas copiadas para que incluso pudiera regresar de funciones anidadas en profundidad. Pero esta versión fue rechazada por el comité de C ++. Puede obtener esta implementación, por ejemplo, con la biblioteca de fibra Boosts.


1

Se supone que las corrutinas son (en C ++) funciones que pueden "esperar" a que se complete alguna otra rutina y proporcionar lo que sea necesario para que la rutina suspendida, en pausa, en espera continúe. la característica que es más interesante para la gente de C ++ es que las corrutinas idealmente no ocuparían espacio en la pila ... C # ya puede hacer algo como esto con await y yield, pero es posible que C ++ tenga que ser reconstruido para incorporarlo.

La concurrencia se centra en gran medida en la separación de preocupaciones cuando una preocupación es una tarea que se supone que el programa debe completar. esta separación de preocupaciones puede lograrse por varios medios ... por lo general, puede ser una delegación de algún tipo. la idea de concurrencia es que una serie de procesos podrían ejecutarse de forma independiente (separación de preocupaciones) y un 'oyente' dirigiría lo que sea producido por esas preocupaciones separadas a donde se supone que debe ir. esto depende en gran medida de algún tipo de gestión asincrónica. Hay varios enfoques para la concurrencia, incluida la programación orientada a aspectos y otros. C # tiene el operador 'delegado' que funciona bastante bien.

El paralelismo suena como concurrencia y puede estar involucrado, pero en realidad es una construcción física que involucra muchos procesadores dispuestos de una manera más o menos paralela con software que es capaz de dirigir partes de código a diferentes procesadores donde se ejecutará y los resultados se recibirán. sincrónicamente.


9
Concurrencia y separación de las preocupaciones son totalmente no relacionado. Las corrutinas no proporcionan información para la rutina suspendida, son las rutinas reanudables.
Ben Voigt
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.