Programmiersprache C++: Concurrency mit std::execution

std::execution bietet ein Standard-C++-Framework fĂĽr die Verwaltung der asynchronen AusfĂĽhrung auf generischen AusfĂĽhrungsressourcen.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Bahngleise

(Bild: heise online / anw)

Lesezeit: 4 Min.
Von
  • Rainer Grimm

Für diesen Beitrag gibt es eine Planänderung. Mein ursprünglicher Plan war es, die C++26-Bibliothek nach der Kernsprache vorzustellen. Der Implementierungsstatus der Bibliothek ist jedoch nicht vollständig genug.

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++.

Daher habe ich mich entschieden, mit Concurrency und std::execution fortzufahren. Ich werde die verbleibenden Features von C++26 vorstellen, sobald ein Compiler sie implementiert.

std::execution hat drei Schlüsselabstraktionen: Scheduler, Sender und Empfänger sowie eine Reihe anpassbarer asynchroner Algorithmen. Meine Präsentation von std::execution basiert auf dem Proposal P2300R10.

Erste Experimente

FĂĽr meine ersten Experimente habe ich stdexec verwendet. Diese Referenzimplementierung von Nvidia basiert auf der achten Revision des Proposals. Der Zweck dieses Experiments findet sich auf GitHub:

  1. Provide a proof-of-concept implementation of the design proposed in P2300.
  2. Provide early access to developers looking to experiment with the Sender model.
  3. Collaborate with those interested in participating or contributing to the design of P2300 (contributions welcome!).

Man kann stdexec auf godbolt mit dem folgenden Programm ausprobieren:

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

int main()
{
    // Declare a pool of 3 worker threads:
    exec::static_thread_pool pool(3);

    // Get a handle to the thread pool:
    auto sched = pool.get_scheduler();

    // Describe some work:
    // Creates 3 sender pipelines that are executed concurrently by passing to `when_all`
    // Each sender is scheduled on `sched` using `on` and starts with `just(n)` that creates a
    // Sender that just forwards `n` to the next sender.
    // After `just(n)`, we chain `then(fun)` which invokes `fun` using the value provided from `just()`
    // Note: No work actually happens here. Everything is lazy and `work` is just an object that statically
    // represents the work to later be executed
    auto fun = [](int i) { return i*i; };
    auto work = stdexec::when_all(
        stdexec::starts_on(sched, stdexec::just(0) | stdexec::then(fun)),
        stdexec::starts_on(sched, stdexec::just(1) | stdexec::then(fun)),
        stdexec::starts_on(sched, stdexec::just(2) | stdexec::then(fun))
    );

    // Launch the work and wait for the result
    auto [i, j, k] = stdexec::sync_wait(std::move(work)).value();

    // Print the results:
    std::printf("%d %d %d\n", i, j, k);
}

Ich werde dieses Programm in die Syntax der Revision 10 konvertieren. Das Programm beginnt mit der Einbindung der erforderlichen Header: <exec/static_thread_pool.hpp> fĂĽr die Erstellung eines Thread-Pools, <stdexec/execution.hpp> fĂĽr ausfĂĽhrungsbezogene Dienste. In der main-Funktion wird ein static_thread_pool pool mit acht Threads erstellt. Der Thread-Pool fĂĽhrt Tasks gleichzeitig aus. Die get_scheduler-Memberfunktion des Thread-Pools wird aufgerufen, um ein Scheduler-Objekt sched zu erhalten. Der Scheduler plant die Tasks im Thread-Pool.

Die Lambda-Funktion fun nimmt eine Ganzzahl i als Eingabe und gibt ihr Quadrat (i * i) zurĂĽck. Dieses Lambda wird auf die Eingabewerte in den nachfolgenden Tasks angewendet. Die Funktion stdexec::when_all erstellt einen Task, der auf den Abschluss mehrerer Unter-Tasks wartet. Jeder Unter-Task wird mit der Funktion stdexec::starts_on erstellt, die den Task auf dem angegebenen Scheduler sched plant. Die Funktion stdexec::just erstellt einen Task, der einen einzelnen Wert (0, 1 oder 2) erzeugt, und die Funktion stdexec::then wird verwendet, um das fun-Lambda auf diesen Wert anzuwenden. Das resultierende Task-Objekt wird work genannt.

Die Funktion stdexec::sync_wait wird dann aufgerufen, um synchron auf die Fertigstellung der Task zu warten. Die Funktion std::move überträgt das Eigentum an des Tasks work an sync_wait. Die value-Memberfunktion wird für das Ergebnis von sync_wait aufgerufen, um die von den Sub-Tasks erzeugten Werte zu erhalten. Diese Werte werden in die Variablen i, jund k entpackt.

SchlieĂźlich gibt das Programm die Werte von i, j und k mit std::printf auf der Konsole aus. Diese Werte stellen die Quadrate von 0, 1 bzw. 2 dar.

Der folgende Screenshot zeigt die AusfĂĽhrung des Programms im Compiler-Explorer:

Ich habe zu Beginn dieses Artikels geschrieben, dass std::execution drei Schlüsselabstraktionen hat: Scheduler, Sender und Empfänger sowie eine Reihe anpassbarer asynchroner Algorithmen.

AusfĂĽhrungsressourcen

  • stellen den Ort der AusfĂĽhrung dar und
  • benötigen keine Darstellung im Code.

Scheduler: sched

  • stellt die AusfĂĽhrungsressource dar.
  • Das Scheduler-Konzept wird durch einen einzigen Senderalgorithmus definiert: schedule.
  • Der Algorithmus schedule gibt einen Sender zurĂĽck, der auf einer vom Scheduler bestimmten AusfĂĽhrungsressource ausgefĂĽhrt wird.

Sender beschreibt Arbeit: when_all, starts_on, just, then

  • sendet einige Werte, wenn ein mit diesem Sender verbundener Empfänger diese Werte schlieĂźlich empfängt,
  • just ist eine sogenannte Senderfabrik.

Empfänger stoppt den Workflow: sync_wait

  • Es unterstĂĽtzt drei Kanäle: Wert, Fehler, gestoppt.
  • Es handelt sich um einen sogenannten Sender-Consumer.
  • Er ĂĽbermittelt die Arbeit und blockiert den aktuellen std::thread und gibt ein optionales Tupel von Werten zurĂĽck, die vom bereitgestellten Sender nach Abschluss der Arbeit gesendet wurden.

Nach dieser Einführung werde ich näher auf die Reihe anpassbarer asynchroner Algorithmen eingehen und weitere Beispiele vorstellen. (rme)