In cosa sono le coroutine c ++ 20?
In che modo è diverso da "Parallelism2" o / e "Concurrency2" (guarda l'immagine sotto)?
L'immagine sotto è da ISOCPP.
In cosa sono le coroutine c ++ 20?
In che modo è diverso da "Parallelism2" o / e "Concurrency2" (guarda l'immagine sotto)?
L'immagine sotto è da ISOCPP.
Risposte:
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 Generator
si 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_yield
insieme 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_yield
sospende l'esecuzione delle funzioni, memorizza tale stato in generator<int>
, quindi restituisce il valore di current
tramite generator<int>
.
È possibile eseguire il ciclo sugli interi restituiti.
co_await
nel 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_await
su 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::future
quando la risorsa nominata viene aperta e riusciamo ad analizzare fino al punto in cui abbiamo trovato i dati richiesti.
open_resource
e read_line
s sono probabilmente coroutine asincrone che aprono un file e leggere le linee da esso. Il co_await
collega lo stato di sospensione e di pronto load_data
al 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_await
e co_yield
significano : ho visto persone usarlo per implementare espressioni opzionali monadiche in modo tale che un co_await
su 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;
}
;;
.
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.
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.