zurück zum Artikel

Ein kleiner Exkurs: Executors

Rainer Grimm

Felix Petriconi, einer der Autoren des Proposals zu Futures, wies unseren Autor darauf hin, dass sein Artikel zu den std::future-Erweiterungen bereits veraltet sei. Denn die Zukunft der Futures hat sich deutlich mit dem neuen Erstarken der Executors verändert.

Vor ein paar Wochen sendete mir Felix Petriconi, einer der Autoren des Proposals zur Futures, eine E-Mail. Er schrieb, dass mein Artikel zu std::future-Erweiterungen [1] bereits veraltet sei. Leider hat er Recht. Die Zukunft der Futures hat sich deutlich mit dem neuen Erstarken der Executors verändert.

Bevor ich über die Zukunft der Futures schreibe, muss ich erst auf das Konzept der Executors eingehen. Sie besitzen eine lange Geschichte in C++, die mindestens acht Jahre alt. Der Vortrag "Finally Executors for C++ [2]" von Detlef Vollmann gibt einen sehr schönen Überblick dazu.

Ein kleiner Ausflug: Executors

Dieser Artikel basiert zu großen Teilen auf dem Proposal P0761 [3] zum Design von Executors und deren formaler Beschreibung im Proposal P0441 [4]. Dieser Artikel bezieht sich auch auf das relativ neue Proposal P1055: [5] Modest Executor Proposal.

Zuerst einmal. Was ist ein Executor?

Sie sind der Grundbaustein, um etwas in C++ auszuführen. Sie nehmen eine ähnliche Rolle wie die Allokatoren für die Container in C++ ein. Zu diesem Zeitpunkt sind bereits einige Proposals zu Executors verfasst worden, und viele Entscheidungen sind noch offen. Die Erwartung ist, dass sie mit C++23 Bestandteil des Standards sind, sie aber als Erweiterung des C++-Standards schon deutlich früher zur Verfügung stehen.

Ein Executor besteht aus einer Menge von Regeln, wo, wann und wie eine aufrufbare Einheit ausgeführt werden soll. Eine aufrufbare Einheit kann eine Funktion, ein Funktionsobjekt oder auch eine Lambda-Funktion sein.

Da Executors der Grundbaustein sind, um etwas auszuführen, hängen die Features zur Concurrency und zur Parallelität in C++ sehr stark von ihnen ab. Das gilt für die neuen Feature zur Concurreny in C++20/23 [6] wie erweiterte Futures, Latches und Barriers, Coroutinen, Transaktional Memory und Task-Blöcke. Dies gilt aber auch für die Erweiterung zur Netzwerk-Programmierung P4734 [7] in C++ und die parallelen Algorithmen der STL [8].

Hier sind ein paar Codebeispiele zur Anwendung des Executors my_executor.

// get an executor through some means
my_executor_type my_executor = ...

// launch an async using my executor
auto future = std::async(my_executor, [] {
std::cout << "Hello world, from a new execution agent!" << std::endl;
});
// get an executor through some means
my_executor_type my_executor = ...

// execute a parallel for_each "on" my executor
std::for_each(std::execution::par.on(my_executor),
data.begin(), data.end(), func);

Es gibt viele Wege, einen Executor zu erhalten:

// create a thread pool with 4 threads
static_thread_pool pool(4);

// get an executor from the thread pool
auto exec = pool.executor();

// use the executor on some long-running task
auto task1 = long_running_task(exec);
// get an executor from a thread pool
auto exec = pool.executor();

// wrap the thread pool's executor in a logging_executor
logging_executor<decltype(exec)> logging_exec(exec);

// use the logging executor in a parallel sort
std::sort(std::execution::par.on(logging_exec),
my_data.begin(), my_data.end());

Der logging-Executor ist in dem Codebeispiel ein Wrapper für den pool-Executor.

Was sind gemäß dem Proposal P1055 [9] die Ziele eines Executor Concept?

Ein Executor bietet mindestens eine der sechs Ausführungsfunktionen an, um aus einer ausführbaren Einheit einen Execution Agent zu erzeugen. Jede Ausführungsfunktion besitzt zwei Eigenschaften: Kardinalität und Richtung:

Ein kleiner Ausflug: Executors

Gerne gehe ich nochmals weniger formal auf die Ausführungsfunktionen ein.

Zuerst beziehe ich mich auf den single-Kardinalität-Fall.

Der bulk-Kardinalität-Fall ist komplizierter. Diese Ausführungsfunktionen erzeugen eine Gruppe von Execution Agents, und jeder dieser Execution Agent ruft die gleiche aufrufbare Einheit auf. Sie geben das Ergebnis einer Fabrik zurück und nicht das Ergebnis der einzelnen Execution Agent. Es liegt in der Verantwortung des Anwenders, das richtige Ergebnis mit Hilfe der Fabrik zu erzeugen.

Wie kannst du dir sicher sein, dass dein Executor die gewünschte Ausführungsfunktion unterstützt? Im konkreten Fall weißt du es:

void concrete_context(const my_oneway_single_executor& ex)
{
auto task = ...;
ex.execute(task);
}

Im allgemeinen Fall kannst du die Funkton execution::require verwenden:

template <typename Executor> 
void generic_context(const Executor& ex)
{
auto task = ...;

// ensure .twoway_execute() is available with execution::require()
execution::require(ex, execution::single,
execution::twoway).twoway_execute(task);
}

In diesem Fall muss der Executor ex die Kardinalität single und die Richtung twoway unterstützten.

In meinem nächsten Artikel geht mein Exkurs von den "C++ Core Guidelines" weiter. Die Zukunft der Futures ändert sich hauptsächlich wegen der Executoren. Daher geht es im nächsten Artikel um die Futures.

Falls du die ganzen Details zu Concurrency von C++11 bis C++20 wissen willst. Mein Buch "Concurrency with Modern C++" gibt es seit heute auch auf Deutsch bei Hanser: "Modernes C++: Concurrency meistern [15]". ( [16])


URL dieses Artikels:
https://www.heise.de/-4075350

Links in diesem Artikel:
[1] https://www.grimm-jaud.de/index.php/blog/std-future-erweiterungen
[2] http://www.vollmann.ch/en/presentations/executors2018.pdf
[3] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0761r2.pdf
[4] http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p0443r7.html
[5] http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1055r0.pdf
[6] https://www.grimm-jaud.de/index.php/blog/category/multithreading-c-17-und-c-20
[7] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4734.pdf
[8] https://www.grimm-jaud.de/index.php/blog/parallele-algorithmen-der-stl
[9] http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1055r0.pdf
[10] https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
[11] https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
[12] https://www.grimm-jaud.de/index.php/blog/std-async-warten-im-destruktor
[13] https://www.grimm-jaud.de/index.php/blog/promise-und-future
[14] https://www.grimm-jaud.de/index.php/blog/promise-und-future
[15] https://www.hanser-fachbuch.de/autor/Rainer+Grimm
[16] mailto:rainer@grimm-jaud.de