Eine sehr gute und leicht zu verfolgende Einführung in Coroutine ist James McNellis 'Präsentation „Einführung in C ++ Coroutines“ (Cppcon2016).
Philsumuru
2
Schließlich wäre es auch gut zu behandeln: "Wie unterscheiden sich Coroutinen in C ++ von den Implementierungen von Koroutinen und wiederaufnehmbaren Funktionen in anderen Sprachen?" (was der oben verlinkte Wikipedia-Artikel, der sprachunabhängig ist, nicht anspricht)
Ben Voigt
1
Wer hat diese "Quarantäne in C ++ 20" noch gelesen?
Sahib Yar
Antworten:
197
Auf einer abstrakten Ebene trennte Coroutines die Idee eines Ausführungszustands von der Idee eines Ausführungsthreads.
SIMD (Single Instruction Multiple Data) hat mehrere "Ausführungsthreads", aber nur einen Ausführungsstatus (es funktioniert nur mit mehreren Daten). Vermutlich sind parallele Algorithmen insofern ein bisschen so, als Sie ein "Programm" mit unterschiedlichen Daten ausführen lassen.
Threading hat mehrere "Ausführungsthreads" und mehrere Ausführungszustände. Sie haben mehr als ein Programm und mehr als einen Ausführungsthread.
Coroutines hat mehrere Ausführungszustände, besitzt jedoch keinen Ausführungsthread. Sie haben ein Programm und das Programm hat den Status, aber keinen Ausführungsthread.
Das einfachste Beispiel für Coroutinen sind Generatoren oder Aufzählungen aus anderen Sprachen.
Im Pseudocode:
function Generator(){for(i =0 to 100)
produce i
}
Das Generator wird aufgerufen und beim ersten Aufruf wird es zurückgegeben0 . Der Status wird gespeichert (wie stark der Status mit der Implementierung von Coroutinen variiert), und wenn Sie ihn das nächste Mal aufrufen, wird er dort fortgesetzt, wo er aufgehört hat. So wird beim nächsten Mal 1 zurückgegeben. Dann 2.
Schließlich erreicht es das Ende der Schleife und fällt vom Ende der Funktion ab; Die Coroutine ist fertig. (Was hier passiert, hängt von der Sprache ab, über die wir sprechen. In Python wird eine Ausnahme ausgelöst.)
Coroutinen bringen diese Funktion in C ++.
Es gibt zwei Arten von Coroutinen; stapelbar und stapellos.
Eine stapellose Coroutine speichert nur lokale Variablen in ihrem Status und ihrem Ausführungsort.
Eine stapelbare Coroutine speichert einen gesamten Stapel (wie einen Thread).
Stapellose Coroutinen können extrem leicht sein. Der letzte Vorschlag, den ich gelesen habe, bestand darin, Ihre Funktion in etwas wie ein Lambda umzuschreiben. Alle lokalen Variablen werden in den Status eines Objekts versetzt, und Beschriftungen werden verwendet, um zu / von der Stelle zu springen, an der die Coroutine Zwischenergebnisse "erzeugt".
Der Prozess der Wertgenerierung wird als "Ertrag" bezeichnet, da Coroutinen ein bisschen wie kooperatives Multithreading sind. Sie geben den Ausführungspunkt an den Anrufer zurück.
Boost hat eine Implementierung von stapelbaren Coroutinen; Sie können eine Funktion aufrufen, die für Sie nachgibt. Stapelbare Coroutinen sind leistungsfähiger, aber auch teurer.
Coroutinen sind mehr als ein einfacher Generator. Sie können eine Coroutine in einer Coroutine abwarten, mit der Sie Coroutinen auf nützliche Weise zusammenstellen können.
Coroutinen wie if, Schleifen und Funktionsaufrufe sind eine andere Art von "strukturiertem goto", mit dem Sie bestimmte nützliche Muster (wie Zustandsmaschinen) auf natürlichere Weise ausdrücken können.
Die spezifische Implementierung von Coroutines in C ++ ist etwas interessant.
Auf der einfachsten Ebene werden C ++: einige Schlüsselwörter hinzugefügt co_returnco_awaitco_yield, zusammen mit einigen Bibliothekstypen, die mit ihnen arbeiten.
Eine Funktion wird zu einer Coroutine, indem sie eine davon in ihrem Körper hat. Von ihrer Deklaration sind sie also nicht von Funktionen zu unterscheiden.
Wenn eines dieser drei Schlüsselwörter in einem Funktionskörper verwendet wird, erfolgt eine standardmäßige Prüfung des Rückgabetyps und der Argumente, und die Funktion wird in eine Coroutine umgewandelt. Diese Prüfung teilt dem Compiler mit, wo der Funktionsstatus gespeichert werden soll, wenn die Funktion angehalten wird.
co_yieldUnterbricht die Funktionsausführung, speichert diesen Status in der generator<int>und gibt dann den Wert von currentdurch zurück generator<int>.
Sie können die zurückgegebenen Ganzzahlen durchlaufen.
co_awaitIn der Zwischenzeit können Sie eine Coroutine auf eine andere spleißen. Wenn Sie sich in einer Coroutine befinden und die Ergebnisse einer erwarteten Sache (häufig einer Coroutine) benötigen, bevor Sie fortfahren, können Sie co_awaitdiese durchführen. Wenn sie bereit sind, fahren Sie sofort fort. Wenn nicht, setzen Sie aus, bis die Wartezeit, auf die Sie warten, bereit ist.
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_dataist eine Coroutine, die std::futurebeim Öffnen der genannten Ressource eine generiert und es uns gelingt, bis zu dem Punkt zu analysieren, an dem wir die angeforderten Daten gefunden haben.
open_resourceund read_lines sind wahrscheinlich asynchrone Coroutinen, die eine Datei öffnen und Zeilen daraus lesen. Das co_awaitverbindet den Suspendierungs- und Bereitschaftszustand load_datamit ihrem Fortschritt.
C ++ - Coroutinen sind viel flexibler als diese, da sie als minimaler Satz von Sprachfunktionen zusätzlich zu den User-Space-Typen implementiert wurden. Die User-Space-Typen definieren effektiv, was co_returnco_awaitund was co_yieldbedeutet. Ich habe gesehen, dass Leute damit monadische optionale Ausdrücke implementieren, sodass co_awaitein leeres optionales automatisch den leeren Status auf das äußere optionale überträgt:
modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ){return(co_await a)+(co_await b);}
anstatt
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;}
Dies ist eine der klarsten Erklärungen dafür, was Coroutinen sind, die ich jemals gelesen habe. Es war eine hervorragende Idee, sie mit SIMD- und klassischen Threads zu vergleichen und von diesen zu unterscheiden.
Omnifarious
2
Ich verstehe das Beispiel für Add-Optionen nicht. std :: optional <int> ist kein zu erwartendes Objekt.
Jive Dadson
1
@mord Ja, es soll 1 Element zurückgeben. Möglicherweise muss poliert werden. Wenn wir mehr als eine Linie wollen, brauchen wir einen anderen Kontrollfluss.
Yakk - Adam Nevraumont
1
@lf sorry, sollte sein ;;.
Yakk - Adam Nevraumont
1
@LF für solch eine einfache Funktion gibt es vielleicht keinen Unterschied. Aber der Unterschied, den ich im Allgemeinen sehe, besteht darin, dass sich eine Coroutine an den Eintritts- / Austrittspunkt (Ausführungspunkt) in ihrem Körper erinnert, während eine statische Funktion die Ausführung jedes Mal von vorne beginnt. Der Ort der "lokalen" Daten ist vermutlich irrelevant.
Avp
20
Eine Coroutine ist wie eine C-Funktion, die mehrere return-Anweisungen hat und beim zweiten Aufruf nicht mit der Ausführung am Anfang der Funktion beginnt, sondern beim ersten Befehl nach der zuvor ausgeführten Rückgabe. Dieser Ausführungsort wird zusammen mit allen automatischen Variablen gespeichert, die in Nicht-Coroutine-Funktionen auf dem Stapel leben würden.
In einer früheren experimentellen Coroutine-Implementierung von Microsoft wurden kopierte Stapel verwendet, sodass Sie sogar von tief verschachtelten Funktionen zurückkehren konnten. Diese Version wurde jedoch vom C ++ - Komitee abgelehnt. Sie können diese Implementierung beispielsweise mit der Boosts-Glasfaserbibliothek erhalten.
Coroutinen sollen (in C ++) Funktionen sein, die in der Lage sind, auf den Abschluss einer anderen Routine zu "warten" und alles bereitzustellen, was für die Fortsetzung der angehaltenen, angehaltenen, wartenden Routine erforderlich ist. Die Funktion, die für C ++ - Leute am interessantesten ist, ist, dass Coroutinen idealerweise keinen Stapelspeicherplatz beanspruchen würden ... C # kann bereits so etwas mit Warten und Ertrag tun, aber C ++ muss möglicherweise neu erstellt werden, um es zu erhalten.
Die Parallelität konzentriert sich stark auf die Trennung von Bedenken, wenn eine Sorge eine Aufgabe ist, die das Programm erfüllen soll. Diese Trennung von Bedenken kann durch eine Reihe von Mitteln erreicht werden ... normalerweise durch eine Art Delegation. Die Idee der Parallelität besteht darin, dass eine Reihe von Prozessen unabhängig voneinander ausgeführt werden können (Trennung von Bedenken) und ein „Zuhörer“ alles, was von diesen getrennten Bedenken erzeugt wird, dahin lenken würde, wohin es gehen soll. Dies hängt stark von einer Art asynchroner Verwaltung ab. Es gibt eine Reihe von Ansätzen zur Parallelität, einschließlich aspektorientierter Programmierung und anderer. C # hat den 'Delegate'-Operator, der ganz gut funktioniert.
Parallelität klingt nach Parallelität und kann involviert sein, ist jedoch ein physisches Konstrukt, an dem viele Prozessoren beteiligt sind, die mehr oder weniger parallel zu Software angeordnet sind und Teile des Codes an verschiedene Prozessoren weiterleiten können, auf denen er ausgeführt wird und die Ergebnisse zurückerhalten werden synchron.
Concurrency und Trennung von Bedenken sind völlig unabhängig. Coroutinen sollen keine Informationen für die angehaltene Routine bereitstellen, sondern sind die wiederaufnehmbaren Routinen.
Antworten:
Auf einer abstrakten Ebene trennte Coroutines die Idee eines Ausführungszustands von der Idee eines Ausführungsthreads.
SIMD (Single Instruction Multiple Data) hat mehrere "Ausführungsthreads", aber nur einen Ausführungsstatus (es funktioniert nur mit mehreren Daten). Vermutlich sind parallele Algorithmen insofern ein bisschen so, als Sie ein "Programm" mit unterschiedlichen Daten ausführen lassen.
Threading hat mehrere "Ausführungsthreads" und mehrere Ausführungszustände. Sie haben mehr als ein Programm und mehr als einen Ausführungsthread.
Coroutines hat mehrere Ausführungszustände, besitzt jedoch keinen Ausführungsthread. Sie haben ein Programm und das Programm hat den Status, aber keinen Ausführungsthread.
Das einfachste Beispiel für Coroutinen sind Generatoren oder Aufzählungen aus anderen Sprachen.
Im Pseudocode:
Das
Generator
wird aufgerufen und beim ersten Aufruf wird es zurückgegeben0
. Der Status wird gespeichert (wie stark der Status mit der Implementierung von Coroutinen variiert), und wenn Sie ihn das nächste Mal aufrufen, wird er dort fortgesetzt, wo er aufgehört hat. So wird beim nächsten Mal 1 zurückgegeben. Dann 2.Schließlich erreicht es das Ende der Schleife und fällt vom Ende der Funktion ab; Die Coroutine ist fertig. (Was hier passiert, hängt von der Sprache ab, über die wir sprechen. In Python wird eine Ausnahme ausgelöst.)
Coroutinen bringen diese Funktion in C ++.
Es gibt zwei Arten von Coroutinen; stapelbar und stapellos.
Eine stapellose Coroutine speichert nur lokale Variablen in ihrem Status und ihrem Ausführungsort.
Eine stapelbare Coroutine speichert einen gesamten Stapel (wie einen Thread).
Stapellose Coroutinen können extrem leicht sein. Der letzte Vorschlag, den ich gelesen habe, bestand darin, Ihre Funktion in etwas wie ein Lambda umzuschreiben. Alle lokalen Variablen werden in den Status eines Objekts versetzt, und Beschriftungen werden verwendet, um zu / von der Stelle zu springen, an der die Coroutine Zwischenergebnisse "erzeugt".
Der Prozess der Wertgenerierung wird als "Ertrag" bezeichnet, da Coroutinen ein bisschen wie kooperatives Multithreading sind. Sie geben den Ausführungspunkt an den Anrufer zurück.
Boost hat eine Implementierung von stapelbaren Coroutinen; Sie können eine Funktion aufrufen, die für Sie nachgibt. Stapelbare Coroutinen sind leistungsfähiger, aber auch teurer.
Coroutinen sind mehr als ein einfacher Generator. Sie können eine Coroutine in einer Coroutine abwarten, mit der Sie Coroutinen auf nützliche Weise zusammenstellen können.
Coroutinen wie if, Schleifen und Funktionsaufrufe sind eine andere Art von "strukturiertem goto", mit dem Sie bestimmte nützliche Muster (wie Zustandsmaschinen) auf natürlichere Weise ausdrücken können.
Die spezifische Implementierung von Coroutines in C ++ ist etwas interessant.
Auf der einfachsten Ebene werden C ++: einige Schlüsselwörter hinzugefügt
co_return
co_await
co_yield
, zusammen mit einigen Bibliothekstypen, die mit ihnen arbeiten.Eine Funktion wird zu einer Coroutine, indem sie eine davon in ihrem Körper hat. Von ihrer Deklaration sind sie also nicht von Funktionen zu unterscheiden.
Wenn eines dieser drei Schlüsselwörter in einem Funktionskörper verwendet wird, erfolgt eine standardmäßige Prüfung des Rückgabetyps und der Argumente, und die Funktion wird in eine Coroutine umgewandelt. Diese Prüfung teilt dem Compiler mit, wo der Funktionsstatus gespeichert werden soll, wenn die Funktion angehalten wird.
Die einfachste Coroutine ist ein Generator:
co_yield
Unterbricht die Funktionsausführung, speichert diesen Status in dergenerator<int>
und gibt dann den Wert voncurrent
durch zurückgenerator<int>
.Sie können die zurückgegebenen Ganzzahlen durchlaufen.
co_await
In der Zwischenzeit können Sie eine Coroutine auf eine andere spleißen. Wenn Sie sich in einer Coroutine befinden und die Ergebnisse einer erwarteten Sache (häufig einer Coroutine) benötigen, bevor Sie fortfahren, können Sieco_await
diese durchführen. Wenn sie bereit sind, fahren Sie sofort fort. Wenn nicht, setzen Sie aus, bis die Wartezeit, auf die Sie warten, bereit ist.load_data
ist eine Coroutine, diestd::future
beim Öffnen der genannten Ressource eine generiert und es uns gelingt, bis zu dem Punkt zu analysieren, an dem wir die angeforderten Daten gefunden haben.open_resource
undread_line
s sind wahrscheinlich asynchrone Coroutinen, die eine Datei öffnen und Zeilen daraus lesen. Dasco_await
verbindet den Suspendierungs- und Bereitschaftszustandload_data
mit ihrem Fortschritt.C ++ - Coroutinen sind viel flexibler als diese, da sie als minimaler Satz von Sprachfunktionen zusätzlich zu den User-Space-Typen implementiert wurden. Die User-Space-Typen definieren effektiv, was
co_return
co_await
und wasco_yield
bedeutet. Ich habe gesehen, dass Leute damit monadische optionale Ausdrücke implementieren, sodassco_await
ein leeres optionales automatisch den leeren Status auf das äußere optionale überträgt:anstatt
quelle
;;
.Eine Coroutine ist wie eine C-Funktion, die mehrere return-Anweisungen hat und beim zweiten Aufruf nicht mit der Ausführung am Anfang der Funktion beginnt, sondern beim ersten Befehl nach der zuvor ausgeführten Rückgabe. Dieser Ausführungsort wird zusammen mit allen automatischen Variablen gespeichert, die in Nicht-Coroutine-Funktionen auf dem Stapel leben würden.
In einer früheren experimentellen Coroutine-Implementierung von Microsoft wurden kopierte Stapel verwendet, sodass Sie sogar von tief verschachtelten Funktionen zurückkehren konnten. Diese Version wurde jedoch vom C ++ - Komitee abgelehnt. Sie können diese Implementierung beispielsweise mit der Boosts-Glasfaserbibliothek erhalten.
quelle
Coroutinen sollen (in C ++) Funktionen sein, die in der Lage sind, auf den Abschluss einer anderen Routine zu "warten" und alles bereitzustellen, was für die Fortsetzung der angehaltenen, angehaltenen, wartenden Routine erforderlich ist. Die Funktion, die für C ++ - Leute am interessantesten ist, ist, dass Coroutinen idealerweise keinen Stapelspeicherplatz beanspruchen würden ... C # kann bereits so etwas mit Warten und Ertrag tun, aber C ++ muss möglicherweise neu erstellt werden, um es zu erhalten.
Die Parallelität konzentriert sich stark auf die Trennung von Bedenken, wenn eine Sorge eine Aufgabe ist, die das Programm erfüllen soll. Diese Trennung von Bedenken kann durch eine Reihe von Mitteln erreicht werden ... normalerweise durch eine Art Delegation. Die Idee der Parallelität besteht darin, dass eine Reihe von Prozessen unabhängig voneinander ausgeführt werden können (Trennung von Bedenken) und ein „Zuhörer“ alles, was von diesen getrennten Bedenken erzeugt wird, dahin lenken würde, wohin es gehen soll. Dies hängt stark von einer Art asynchroner Verwaltung ab. Es gibt eine Reihe von Ansätzen zur Parallelität, einschließlich aspektorientierter Programmierung und anderer. C # hat den 'Delegate'-Operator, der ganz gut funktioniert.
Parallelität klingt nach Parallelität und kann involviert sein, ist jedoch ein physisches Konstrukt, an dem viele Prozessoren beteiligt sind, die mehr oder weniger parallel zu Software angeordnet sind und Teile des Codes an verschiedene Prozessoren weiterleiten können, auf denen er ausgeführt wird und die Ergebnisse zurückerhalten werden synchron.
quelle