Was sind Coroutinen in c ++ 20?
Inwiefern unterscheidet es sich von "Parallelism2" oder / und "Concurrency2" (siehe Bild unten)?
Das folgende Bild stammt von ISOCPP.
Was sind Coroutinen in c ++ 20?
Inwiefern unterscheidet es sich von "Parallelism2" oder / und "Concurrency2" (siehe Bild unten)?
Das folgende Bild stammt von ISOCPP.
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:
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_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:
generator<int> get_integers( int start=0, int step=1 ) {
for (int current=start; true; current+= step)
co_yield current;
}
co_yield
Unterbricht die Funktionsausführung, speichert diesen Status in der generator<int>
und gibt dann den Wert von current
durch zurück generator<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 Sie co_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.
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
ist eine Coroutine, die std::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
und read_line
s sind wahrscheinlich asynchrone Coroutinen, die eine Datei öffnen und Zeilen daraus lesen. Das co_await
verbindet den Suspendierungs- und Bereitschaftszustand load_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 was co_yield
bedeutet. Ich habe gesehen, dass Leute damit monadische optionale Ausdrücke implementieren, sodass co_await
ein 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;
}
;;
.
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.