Cosa sono le coroutine in C ++ 20?


104

In cosa sono le coroutine ?

In che modo è diverso da "Parallelism2" o / e "Concurrency2" (guarda l'immagine sotto)?

L'immagine sotto è da ISOCPP.

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

inserisci qui la descrizione dell'immagine


3
Per rispondere "In che modo il concetto di coroutine è diverso dal parallelismo e dalla concorrenza ?" - en.wikipedia.org/wiki/Coroutine
Ben Voigt


3
Un'introduzione alla coroutine molto buona e facile da seguire è la presentazione di James McNellis "Introduction to C ++ Coroutines" (Cppcon2016).
philsumuru

2
Infine sarebbe anche utile trattare "In che modo le coroutine in C ++ sono diverse dalle implementazioni di coroutine e funzioni ripristinabili in altri linguaggi?" (che l'articolo di wikipedia sopra collegato, essendo indipendente dalla lingua, non affronta)
Ben Voigt

1
chi altro ha letto questa "quarantena in C ++ 20"?
Sahib Yar

Risposte:


199

A livello astratto, Coroutines ha separato l'idea di avere uno stato di esecuzione dall'idea di avere un filo di esecuzione.

SIMD (singola istruzione più dati) ha più "thread di esecuzione" ma solo uno stato di esecuzione (funziona solo su più dati). Probabilmente gli algoritmi paralleli sono un po 'così, in quanto hai un "programma" eseguito su dati diversi.

Il threading ha più "thread di esecuzione" e più stati di esecuzione. Hai più di un programma e più di un thread di esecuzione.

Coroutines ha più stati di esecuzione, ma non possiede un thread di esecuzione. Hai un programma e il programma ha uno stato, ma non ha thread di esecuzione.


L'esempio più semplice di coroutine sono generatori o enumerabili da altre lingue.

In pseudo codice:

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

La Generatorsi chiama, e la prima volta che viene chiamato lo restituisce 0. Il suo stato viene ricordato (quanto lo stato varia con l'implementazione delle coroutine) e la volta successiva che lo chiami continua da dove era stato interrotto. Quindi restituisce 1 la volta successiva. Quindi 2.

Infine raggiunge la fine del ciclo e cade dalla fine della funzione; la coroutine è finita. (Quello che succede qui varia in base al linguaggio di cui stiamo parlando; in Python, genera un'eccezione).

Le coroutine portano questa capacità in C ++.

Esistono due tipi di coroutine; impilabile e impilabile.

Una coroutine stackless memorizza solo le variabili locali nel suo stato e nella sua posizione di esecuzione.

Una coroutine impilata memorizza un intero stack (come un thread).

Le coroutine impilabili possono essere estremamente leggere. L'ultima proposta che ho letto riguardava fondamentalmente la riscrittura della tua funzione in qualcosa di simile a un lambda; tutte le variabili locali entrano nello stato di un oggetto e le etichette vengono utilizzate per saltare alla / dalla posizione in cui la coroutine "produce" risultati intermedi.

Il processo di produzione di un valore è chiamato "rendimento", poiché le coroutine sono un po 'come il multithreading cooperativo; stai restituendo il punto di esecuzione al chiamante.

Boost ha un'implementazione di coroutine impilate; ti consente di chiamare una funzione da restituire. Le coroutine impilate sono più potenti, ma anche più costose.


C'è di più nelle coroutine di un semplice generatore. Puoi aspettare una coroutine in una coroutine, che ti consente di comporre coroutine in modo utile.

Le coroutine, come if, i loop e le chiamate di funzione, sono un altro tipo di "goto strutturato" che ti permette di esprimere certi pattern utili (come le macchine a stati) in modo più naturale.


L'implementazione specifica di Coroutines in C ++ è un po 'interessante.

Al suo livello più elementare, aggiunge alcune parole chiave a C ++:, co_return co_await co_yieldinsieme ad alcuni tipi di libreria che funzionano con loro.

Una funzione diventa una coroutine avendo una di quelle nel suo corpo. Quindi dalla loro dichiarazione sono indistinguibili dalle funzioni.

Quando una di queste tre parole chiave viene utilizzata nel corpo di una funzione, si verifica un esame obbligatorio standard del tipo e degli argomenti restituiti e la funzione viene trasformata in una coroutine. Questo esame indica al compilatore dove memorizzare lo stato della funzione quando la funzione è sospesa.

La coroutine più semplice è un generatore:

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

co_yieldsospende l'esecuzione delle funzioni, memorizza tale stato in generator<int>, quindi restituisce il valore di currenttramite generator<int>.

È possibile eseguire il ciclo sugli interi restituiti.

co_awaitnel frattempo ti permette di unire una coroutine all'altra. Se sei in una coroutine e hai bisogno dei risultati di una cosa attendibile (spesso una coroutine) prima di progredire, tu co_awaitsu di essa. Se sono pronti, procedi immediatamente; in caso contrario, sospendi fino a quando l'atteso che stai aspettando non è pronto.

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_dataè una coroutine che genera un std::futurequando la risorsa nominata viene aperta e riusciamo ad analizzare fino al punto in cui abbiamo trovato i dati richiesti.

open_resourcee read_lines sono probabilmente coroutine asincrone che aprono un file e leggere le linee da esso. Il co_awaitcollega lo stato di sospensione e di pronto load_dataal loro progresso.

Le coroutine C ++ sono molto più flessibili di così, poiché sono state implementate come un set minimo di funzionalità del linguaggio in cima ai tipi di spazio utente. I tipi di spazio utente definiscono efficacemente cosa co_return co_awaite co_yield significano : ho visto persone usarlo per implementare espressioni opzionali monadiche in modo tale che un co_awaitsu un opzionale vuoto proponga automaticamente lo stato vuoto all'opzionale esterno:

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

invece di

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
Questa è una delle spiegazioni più chiare di cosa sono le coroutine che io abbia mai letto. Confrontarli e distinguerli dai thread SIMD e classici è stata un'idea eccellente.
Omnifarious

2
Non capisco l'esempio di add-optionals. std :: optional <int> non è un oggetto awaitable.
Jive Dadson

1
@mord yes dovrebbe restituire 1 elemento. Potrebbe aver bisogno di lucidatura; se vogliamo più di una linea è necessario un flusso di controllo diverso.
Yakk - Adam Nevraumont

1
@lf sorry, should be ;;.
Yakk - Adam Nevraumont

1
@LF per una funzione così semplice forse non c'è differenza. Ma la differenza che vedo in generale è che una coroutine ricorda il punto di entrata / uscita (esecuzione) nel suo corpo mentre una funzione statica avvia l'esecuzione dall'inizio ogni volta. La posizione dei dati "locali" è irrilevante immagino.
avp

21

Una coroutine è come una funzione C che ha più istruzioni di ritorno e quando viene chiamata una seconda volta non inizia l'esecuzione all'inizio della funzione ma alla prima istruzione dopo il precedente ritorno eseguito. Questa posizione di esecuzione viene salvata insieme a tutte le variabili automatiche che risiederebbero sullo stack in funzioni non coroutine.

Una precedente implementazione sperimentale di coroutine di Microsoft utilizzava stack copiati in modo da poter tornare anche da funzioni nidificate in profondità. Ma questa versione è stata rifiutata dal comitato C ++. È possibile ottenere questa implementazione ad esempio con la libreria di fibre Boosts.


1

si suppone che le coroutine siano (in C ++) funzioni che sono in grado di "attendere" il completamento di qualche altra routine e di fornire tutto ciò che è necessario affinché la routine sospesa, in pausa, in attesa continui. la caratteristica che è più interessante per la gente di C ++ è che le coroutine idealmente non occuperebbero spazio nello stack ... C # può già fare qualcosa di simile con attesa e resa, ma potrebbe essere necessario ricostruire C ++ per ottenerlo.

la concorrenza è fortemente focalizzata sulla separazione delle preoccupazioni in cui una preoccupazione è un'attività che il programma dovrebbe completare. questa separazione delle preoccupazioni può essere ottenuta con diversi mezzi ... di solito è una delega di qualche tipo. l'idea di concorrenza è che un certo numero di processi potrebbe essere eseguito in modo indipendente (separazione delle preoccupazioni) e un "ascoltatore" dirigerebbe tutto ciò che è prodotto da quelle preoccupazioni separate ovunque dovrebbe andare. questo dipende fortemente da una sorta di gestione asincrona. Esistono numerosi approcci alla concorrenza, inclusa la programmazione orientata agli aspetti e altri. C # ha l'operatore "delegato" che funziona abbastanza bene.

il parallelismo suona come la concorrenza e può essere coinvolto ma in realtà è un costrutto fisico che coinvolge molti processori disposti in modo più o meno parallelo con un software che è in grado di dirigere porzioni di codice a diversi processori dove verrà eseguito e i risultati verranno ricevuti indietro sincrono.


9
La concorrenza e la separazione delle preoccupazioni sono totalmente indipendenti. Le routine non forniscono informazioni per la routine sospesa, sono le routine ripristinabili.
Ben Voigt
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.