Programmiersprache C++: cstd::execution: Asynchrone Algorithmen

Das Standard-C++-Framework std::execution bietet viele asynchrone Algorithmen fĂĽr diverse Aufgaben.

In Pocket speichern vorlesen Druckansicht 5 Kommentare lesen
HolzwĂĽrfel C++

(Bild: SerbioVas/Shutterstock)

Lesezeit: 4 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Nach der allgemeinen Vorstellung von std::execution in meinem vorherigen Blogbeitrag widme ich mich den anpassbaren asynchronen Algorithmen.

Es ist nicht einfach, das Proposal P2300R10 vorzustellen. Erstens ist es leistungsstark und zweitens sehr lang. Daher konzentriere ich mich auf bestimmte Aspekte.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Für das Proposal gilt eine Reihe von Prioritäten. Das C++-Modell für asynchronen Code soll

  • zusammensetzbar und generisch sein, sodass Benutzer Code schreiben können, der mit vielen verschiedenen Arten von AusfĂĽhrungsressourcen verwendet werden kann.
  • gemeinsame asynchrone Muster in anpassbaren und wiederverwendbaren Algorithmen zusammenfassen, damit Benutzer nicht alles selbst erfinden mĂĽssen.
  • es einfach machen, durch Konstruktion korrekt zu sein.
  • die Vielfalt der AusfĂĽhrungsressourcen und AusfĂĽhrungsagenten unterstĂĽtzen, da nicht alle AusfĂĽhrungsagenten gleich sind; einige sind weniger leistungsfähig als andere, aber nicht weniger wichtig.
  • ermöglichen, dass alles von einer AusfĂĽhrungsressource angepasst werden kann, einschlieĂźlich der Ăśbertragung an andere AusfĂĽhrungsressourcen, aber nicht verlangen, dass AusfĂĽhrungsressourcen alles anpassen –
    dabei alle sinnvollen Anwendungsfälle, Domänen und Plattformen berücksichtigen.
  • dafĂĽr sorgen, dass Fehler weitergegeben werden, aber die Fehlerbehandlung keine Belastung darstellt.
  • den Abbruch, der kein Fehler ist, unterstĂĽtzen.
  • klare und präzise Antworten darauf haben, wo Dinge ausgefĂĽhrt werden.
  • in der Lage sein, die Lebensdauern von Objekten asynchron zu verwalten und zu beenden.

Die Begriffe Ausführungsressource, Ausführungsagent und Scheduler sind für das Verständnis von std::execution unerlässlich. Hier sind die ersten vereinfachten Definitionen.

Eine Ausführungsressource ist eine Programmressourceneinheit, die eine Reihe von Ausführungsagenten verwaltet. Beispiele für Ausführungsressourcen sind der aktive Thread, ein Thread-Pool oder ein zusätzlicher Hardwarebeschleuniger.

Jeder Funktionsaufruf wird in einem AusfĂĽhrungsagenten ausgefĂĽhrt.

Ein Scheduler ist eine Abstraktion einer AusfĂĽhrungsressource mit einer einheitlichen, generischen Schnittstelle fĂĽr die Planung von Arbeit auf dieser Ressource. Er ist eine Fabrik fĂĽr Sender.

Hier ist noch das „Hello World“ Program von std::execution. Dieses Mal lässt sich das Programm im Compiler Explorer ausführen, und meine Analyse wird tiefer gehen.

// HelloWorldExecution.cpp

#include <exec/static_thread_pool.hpp>
#include <iostream>
#include <stdexec/execution.hpp>


int main() {
  exec::static_thread_pool pool(8);
  auto sch = pool.get_scheduler();

  auto begin = stdexec::schedule(sch);
  auto hi = stdexec::then(begin, [] {
    std::cout << "Hello world! Have an int.\n";
    return 13;
  });
  auto add_42 = stdexec::then(hi, [](int arg) { return arg + 42; });

  auto [i] = stdexec::sync_wait(add_42).value();

  std::cout << "i = " << i << '\n';
}

Zunächst einmal ist hier die Ausgabe des Programms:

Das Programm beginnt mit der Einbindung der erforderlichen Header: <exec/static_thread_pool.hpp> fĂĽr die Erstellung eines Thread-Pools und <stdexec/execution.hpp> fĂĽr ausfĂĽhrungsbezogene Utilities.

In der main Funktion wird ein static_thread_pool pool mit acht Threads erstellt.

Die Memberfunktion get_scheduler des Thread-Pools wird aufgerufen, um ein leichtgewichtiges Handle für das Ausführungsressourcen-Scheduler namens sch zu erhalten, das zur Planung von Sendern im Thread-Pool verwendet wird. In diesem Fall ist die Ausführungsressource ein Thread-Pool, es könnte sich aber auch um den Haupt-Thread, die GPU oder ein Task-Framework handeln

Das Programm erstellt dann eine Reihe von Sendern, die von AusfĂĽhrungsagenten ausgefĂĽhrt werden.

Der erste Sender begin wird mit der Funktion stdexec::schedule erstellt, die einen Sender auf dem angegebenen Scheduler sch plant. stdexec::schedule ist eine sogenannte Senderfabrik. Es gibt noch weitere Senderfabriken. Ich verwende den Namensraum des kommenden Standards:

Der nächste Sender hi verwendet den Senderadapter stdexec::then, der den Sender begin und eine Lambda-Funktion verwendet. Diese Lambda-Funktion gibt „Hello world! Have an int.“ an die Konsole aus und gibt den Integer-Wert 13 zurück. Der dritte Sender add_42 wird ebenfalls mit dem Sender-Adapter stdexec::then erstellt. Die Fortsetzung übernimmt die hi Task und ein weiteres Lambda, das ein Integer-Argument arg entgegennimmt und das Ergebnis der Addition von 42 zurückgibt. Sender werden asynchron ausgeführt und sind im Allgemeinen zusammensetzbar.

std::execution bietet weitere Senderadapter:

execution::continues_on
execution::then
execution::upon_*
execution::let_*
execution::starts_on
execution::into_variant
execution::stopped_as_optional
execution::stopped_as_error
execution::bulk
execution::split
execution::when_all

Im Gegensatz zu den Sendern wird der Sender-Consumer synchron ausgefĂĽhrt. Der stdexec::sync_wait-Aufruf wartet auf die Fertigstellung des add_42-Senders. Die value-Methode wird fĂĽr das Ergebnis von sync_wait aufgerufen, um den vom Sender erzeugten Wert zu erhalten, der in die Variable i entpackt wird.

this_thread::sync_wait ist der einzige Sender-Consumer im Execution-Framework.

In meinem nächsten Artikel werde ich einen anspruchsvolleren Algorithmus analysieren. (rme)