Was ist std :: versprechen?

384

Ich bin ziemlich vertraut mit C ++ 11 ist std::thread, std::asyncund std::futureKomponenten (siehe zB diese Antwort ), die geradlinig sind.

Ich kann jedoch nicht genau verstehen std::promise, was es ist, was es tut und in welchen Situationen es am besten verwendet wird. Das Standarddokument selbst enthält nicht viele Informationen, die über die Klassensynopse hinausgehen, und auch nicht nur :: thread .

Könnte jemand bitte ein kurzes, prägnantes Beispiel für eine Situation geben, in der eine std::promisebenötigt wird und in der dies die idiomatischste Lösung ist?

Kerrek SB
quelle
2
Hier ist ein Code dazu in: en.cppreference.com/w/cpp/thread/future
chris
58
Die wirklich, wirklich kurze Version ist: std::promiseist , wo std::futures kommen. std::futureAuf diese Weise können Sie einen Wert abrufen, der Ihnen versprochen wurde . Wenn Sie get()eine Zukunft anrufen , wartet sie, bis der Eigentümer der, std::promisemit der sie den Wert festlegt (indem sie set_valuedas Versprechen aufruft ). Wenn das Versprechen zerstört wird, bevor ein Wert festgelegt wird, und Sie dann get()eine mit diesem Versprechen verbundene Zukunft aufrufen , erhalten Sie eine std::broken_promiseAusnahme, weil Ihnen ein Wert versprochen wurde, aber es ist Ihnen unmöglich, einen zu erhalten.
James McNellis
14
Ich empfehle, wenn Sie können / wollen, einen Blick auf C ++ Concurrency in Action von Anthony Williams
David Rodríguez - Dribeas
32
@KerrekSB std::broken_promiseist der am besten benannte Bezeichner in der Standardbibliothek. Und es gibt keine std::atomic_future.
Cubbi
3
Downvoter, möchten Sie Ihren Einwand erklären?
Kerrek SB

Antworten:

182

In den Worten von [futures.state] std::futureist a ein asynchrones Rückgabeobjekt ("ein Objekt, das Ergebnisse aus einem gemeinsam genutzten Status liest") und a std::promiseist ein asynchroner Anbieter ("ein Objekt, das einem gemeinsam genutzten Status ein Ergebnis liefert"), dh a Versprechen ist es , was Sie setzen ein Ergebnis auf, so dass Sie bekommen es von der zugehörigen Zukunft.

Der asynchrone Anbieter erstellt zunächst den gemeinsam genutzten Status, auf den sich eine Zukunft bezieht. std::promiseist eine Art von asynchronem Anbieter, std::packaged_taskist eine andere und das interne Detail von std::asyncist eine andere. Jeder von ihnen kann einen gemeinsamen Status erstellen und Ihnen einen Status geben std::future, der diesen Status teilt, und den Status bereitstellen.

std::asyncist ein übergeordnetes Dienstprogramm, das Ihnen ein asynchrones Ergebnisobjekt zur Verfügung stellt und sich intern darum kümmert, den asynchronen Anbieter zu erstellen und den freigegebenen Status nach Abschluss der Aufgabe bereit zu stellen. Sie könnten es mit einem std::packaged_task(oder std::bindund einem std::promise) und einem emulieren, std::threadaber es ist sicherer und einfacher zu verwenden std::async.

std::promiseist etwas niedriger, wenn Sie ein asynchrones Ergebnis an die Zukunft übergeben möchten, der Code, der das Ergebnis bereitstellt, jedoch nicht in einer einzigen Funktion zusammengefasst werden kann, die zum Übergeben an geeignet ist std::async. Beispielsweise könnten Sie ein Array von mehreren promises und zugehörigen futures haben und einen einzelnen Thread, der mehrere Berechnungen durchführt und für jedes Versprechen ein Ergebnis festlegt. asyncSie können nur ein einziges Ergebnis zurückgeben, um mehrere zurückzugeben, die Sie asyncmehrmals aufrufen müssen , wodurch möglicherweise Ressourcen verschwendet werden.

Jonathan Wakely
quelle
10
Könnten Ressourcen verschwendet werden? Könnte falsch sein, wenn dieser Code nicht parallelisiert werden kann.
Welpe
"asynchrone Rückgabe" und "Leseergebnis aus gemeinsam genutztem Zustand" sind meist orthogonal, was den ersten Satz etwas verwirrend macht. Wollen Sie damit sagen, dass die Aufteilung des Staates zwischen der Zukunft und dem Versprechen liegt? Wenn ja, sagen Sie das bitte von Anfang an explizit.
Einpoklum
@einpoklum warum hast du vor dem letzten Wort aufgehört, "asynchrones Rückgabeobjekt" zu lesen? Ich zitiere die Terminologie des Standards. A futureist ein konkretes Beispiel für ein asynchrones Rückgabeobjekt , bei dem es sich um ein Objekt handelt, das ein Ergebnis liest, das asynchron über den gemeinsam genutzten Status zurückgegeben wurde. A promiseist ein konkretes Beispiel für einen asynchronen Anbieter , bei dem es sich um ein Objekt handelt, das einen Wert in den gemeinsam genutzten Status schreibt, der asynchron gelesen werden kann. Ich meinte, was ich schrieb.
Jonathan Wakely
496

Ich verstehe die Situation jetzt ein bisschen besser (nicht zuletzt aufgrund der Antworten hier!), Also dachte ich, ich füge selbst ein wenig hinzu.


In C ++ 11 gibt es zwei unterschiedliche, jedoch verwandte Konzepte: Asynchrone Berechnung (eine Funktion, die an einer anderen Stelle aufgerufen wird) und gleichzeitige Ausführung (ein Thread , der gleichzeitig funktioniert). Die beiden sind etwas orthogonale Konzepte. Asynchrone Berechnung ist nur eine andere Variante des Funktionsaufrufs, während ein Thread ein Ausführungskontext ist. Threads sind an sich nützlich, aber für den Zweck dieser Diskussion werde ich sie als Implementierungsdetail behandeln.


Es gibt eine Abstraktionshierarchie für die asynchrone Berechnung. Nehmen wir zum Beispiel an, wir haben eine Funktion, die einige Argumente akzeptiert:

int foo(double, char, bool);

Zunächst haben wir die Vorlage std::future<T>, die einen zukünftigen Wert des Typs darstellt T. Der Wert kann über die Member-Funktion abgerufen werden get(), die das Programm effektiv synchronisiert, indem sie auf das Ergebnis wartet. Alternativ unterstützt eine Zukunft wait_for(), mit der geprüft werden kann, ob das Ergebnis bereits verfügbar ist oder nicht. Futures sollten als asynchroner Drop-In-Ersatz für normale Renditetypen betrachtet werden. Für unsere Beispielfunktion erwarten wir a std::future<int>.

Nun zur Hierarchie von der höchsten zur niedrigsten Ebene:

  1. std::async: Die bequemste und einfachste Möglichkeit, eine asynchrone Berechnung durchzuführen, ist die asyncFunktionsvorlage, die die passende Zukunft sofort zurückgibt:

    auto fut = std::async(foo, 1.5, 'x', false);  // is a std::future<int>

    Wir haben sehr wenig Kontrolle über die Details. Insbesondere wissen wir nicht einmal, ob die Funktion gleichzeitig, seriell get()oder von einer anderen schwarzen Magie ausgeführt wird. Das Ergebnis ist jedoch bei Bedarf leicht zu erhalten:

    auto res = fut.get();  // is an int
  2. Wir können jetzt überlegen, wie wir so etwas implementieren sollenasync , aber auf eine Weise, die wir kontrollieren. Zum Beispiel können wir darauf bestehen, dass die Funktion in einem separaten Thread ausgeführt wird. Wir wissen bereits, dass wir mit Hilfe der std::threadKlasse einen separaten Thread bereitstellen können .

    Die nächst niedrigere Abstraktionsebene macht genau das : std::packaged_task. Dies ist eine Vorlage, die eine Funktion umschließt und eine Zukunft für den Rückgabewert der Funktionen bietet. Das Objekt selbst kann jedoch aufgerufen werden, und der Aufruf liegt im Ermessen des Benutzers. Wir können es so einrichten:

    std::packaged_task<int(double, char, bool)> tsk(foo);
    
    auto fut = tsk.get_future();    // is a std::future<int>

    Die Zukunft wird bereit, sobald wir die Aufgabe aufrufen und der Anruf abgeschlossen ist. Dies ist der ideale Job für einen separaten Thread. Wir müssen nur sicherstellen, dass die Aufgabe in den Thread verschoben wird:

    std::thread thr(std::move(tsk), 1.5, 'x', false);

    Der Thread wird sofort ausgeführt. Wir können es entweder detachoder joinam Ende des Bereichs haben oder wann immer (z. B. mit Anthony Williams ' scoped_threadWrapper, der eigentlich in der Standardbibliothek enthalten sein sollte). Die Details der Verwendung std::threadbetreffen uns hier jedoch nicht; Stellen Sie einfach sicher, dass Sie sich thrirgendwann anschließen oder trennen . Was zählt ist, dass unser Ergebnis immer dann bereit ist, wenn der Funktionsaufruf beendet ist:

    auto res = fut.get();  // as before
  3. Jetzt sind wir auf der untersten Ebene: Wie würden wir die gepackte Aufgabe implementieren ? Hier kommt das ins std::promiseSpiel. Das Versprechen ist der Baustein für die Kommunikation mit einer Zukunft. Die Hauptschritte sind folgende:

    • Der aufrufende Thread macht ein Versprechen.

    • Der aufrufende Thread erhält aus dem Versprechen eine Zukunft.

    • Das Versprechen wird zusammen mit Funktionsargumenten in einen separaten Thread verschoben.

    • Der neue Thread führt die Funktion aus und erfüllt das Versprechen.

    • Der ursprüngliche Thread ruft das Ergebnis ab.

    Als Beispiel ist hier unsere eigene "verpackte Aufgabe":

    template <typename> class my_task;
    
    template <typename R, typename ...Args>
    class my_task<R(Args...)>
    {
        std::function<R(Args...)> fn;
        std::promise<R> pr;             // the promise of the result
    public:
        template <typename ...Ts>
        explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
    
        template <typename ...Ts>
        void operator()(Ts &&... ts)
        {
            pr.set_value(fn(std::forward<Ts>(ts)...));  // fulfill the promise
        }
    
        std::future<R> get_future() { return pr.get_future(); }
    
        // disable copy, default move
    };

    Die Verwendung dieser Vorlage entspricht im Wesentlichen der von std::packaged_task. Beachten Sie, dass das Verschieben der gesamten Aufgabe das Verschieben des Versprechens umfasst. In Ad-hoc-Situationen könnte man ein Versprechungsobjekt auch explizit in den neuen Thread verschieben und es zu einem Funktionsargument der Thread-Funktion machen, aber ein Task-Wrapper wie der obige scheint eine flexiblere und weniger aufdringliche Lösung zu sein.


Ausnahmen machen

Versprechen sind eng mit Ausnahmen verbunden. Die Schnittstelle eines Versprechens allein reicht nicht aus, um seinen Zustand vollständig zu vermitteln. Daher werden Ausnahmen ausgelöst, wenn eine Operation an einem Versprechen keinen Sinn ergibt. Alle Ausnahmen sind vom Typ std::future_error, der sich von ableitet std::logic_error. Zunächst eine Beschreibung einiger Einschränkungen:

  • Ein standardmäßig erstelltes Versprechen ist inaktiv. Inaktive Versprechen können ohne Konsequenz sterben.

  • Ein Versprechen wird aktiv, wenn eine Zukunft über erhalten wird get_future(). Es kann jedoch nur eine Zukunft erhalten werden!

  • Ein Versprechen muss entweder erfüllt werden set_value()oder eine Ausnahme muss festgelegt werden, set_exception()bevor seine Lebensdauer endet, wenn seine Zukunft konsumiert werden soll. Ein erfülltes Versprechen kann ohne Konsequenz sterben und get()wird für die Zukunft verfügbar. Ein Versprechen mit einer Ausnahme löst die gespeicherte Ausnahme beim Aufruf von get()in der Zukunft aus. Wenn das Versprechen weder mit Wert noch mit Ausnahme stirbt, wird durch das Aufrufen get()der Zukunft eine Ausnahme mit "gebrochenem Versprechen" ausgelöst.

Hier sind einige kleine Testreihen, um diese verschiedenen außergewöhnlichen Verhaltensweisen zu demonstrieren. Erstens das Geschirr:

#include <iostream>
#include <future>
#include <exception>
#include <stdexcept>

int test();

int main()
{
    try
    {
        return test();
    }
    catch (std::future_error const & e)
    {
        std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl;
    }
    catch (std::exception const & e)
    {
        std::cout << "Standard exception: " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "Unknown exception." << std::endl;
    }
}

Nun zu den Tests.

Fall 1: Inaktives Versprechen

int test()
{
    std::promise<int> pr;
    return 0;
}
// fine, no problems

Fall 2: Aktives Versprechen, ungenutzt

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();
    return 0;
}
// fine, no problems; fut.get() would block indefinitely

Fall 3: Zu viele Futures

int test()
{
    std::promise<int> pr;
    auto fut1 = pr.get_future();
    auto fut2 = pr.get_future();  //   Error: "Future already retrieved"
    return 0;
}

Fall 4: Erfülltes Versprechen

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_value(10);
    }

    return fut.get();
}
// Fine, returns "10".

Fall 5: Zu viel Zufriedenheit

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_value(10);
        pr2.set_value(10);  // Error: "Promise already satisfied"
    }

    return fut.get();
}

Die gleiche Ausnahme ausgelöst wird , wenn mehr als eine der ist entweder von set_valueoder set_exception.

Fall 6: Ausnahme

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo")));
    }

    return fut.get();
}
// throws the runtime_error exception

Fall 7: Gebrochenes Versprechen

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
    }   // Error: "broken promise"

    return fut.get();
}
Kerrek SB
quelle
Sie sagten: "... was das Programm effektiv synchronisiert, indem es auf das Ergebnis wartet." . Was bedeutet hier "synchronisieren"? Was bedeutet die gesamte Aussage? Ich kann das nicht verstehen. Keine der Bedeutungen von "synchronisieren" aus diesem Wörterbucheintrag hilft mir, den Satz zu verstehen. Bedeutet nur "Warten" "Synchronisation"? Synchronisiert sich jede Wartezeit? Ich glaube, ich verstehe teilweise, was du meinst, aber ich bin mir nicht sicher, was du eigentlich meinst.
Nawaz
9
Schöne Antwort, danke für Ihre Hilfe. Über den Teil von std :: async erinnere ich mich, dass wir feststellen konnten, dass es einen anderen Thread erzeugen oder synchron mit flag arbeiten würde (std :: launch :: async, std :: launch :: deferred)
StereoMatching
1
@ FelixDombek: Perfekte Weiterleitung usw. std::functionhat viele Konstruktoren; Kein Grund, diese nicht dem Verbraucher von my_task.
Kerrek SB
1
@ DaveedV.: Danke für das Feedback! Ja, das ist Testfall 7: Wenn Sie das Versprechen zerstören, ohne einen Wert oder get()eine Ausnahme festzulegen, löst der Aufruf der Zukunft eine Ausnahme aus. Ich werde dies klarstellen, indem ich "bevor es zerstört wird" hinzufüge; Bitte lassen Sie mich wissen, ob dies hinreichend klar ist.
Kerrek SB
3
Zum Schluss got()habe futureich die Thread-Support-Bibliothek über promiseIhre erstaunliche Erklärung informiert !
sonniger Mond
33

Bartosz Milewski liefert eine gute Zusammenfassung.

C ++ teilt die Implementierung von Futures in eine Reihe kleiner Blöcke auf

std :: Versprechen ist einer dieser Teile.

Ein Versprechen ist ein Mittel, um den Rückgabewert (oder eine Ausnahme) vom Thread, der eine Funktion ausführt, an den Thread zu übergeben, der von der zukünftigen Funktion profitiert.

...

Eine Zukunft ist das Synchronisationsobjekt, das um das Empfangsende des Versprechenskanals herum aufgebaut ist.

Wenn Sie also eine Zukunft nutzen möchten, erhalten Sie ein Versprechen, mit dem Sie das Ergebnis der asynchronen Verarbeitung erhalten.

Ein Beispiel von der Seite ist:

promise<int> intPromise;
future<int> intFuture = intPromise.get_future();
std::thread t(asyncFun, std::move(intPromise));
// do some other stuff
int result = intFuture.get(); // may throw MyException
Paul Rubel
quelle
4
Das Versprechen im Konstruktor des Threads zu sehen, ließ den Penny schließlich fallen. Bartosz 'Artikel ist vielleicht nicht der größte, aber er erklärt, wie die Elemente zusammenpassen. Vielen Dank.
Kerrek SB
28

In grober Näherung können Sie std::promiseals das andere Ende von a betrachten std::future(dies ist falsch , aber zur Veranschaulichung können Sie denken, als ob es wäre). Das Consumer-Ende des Kommunikationskanals würde a verwenden std::future, um das Datum aus dem gemeinsam genutzten Status zu verbrauchen, während der Producer-Thread a verwenden würde std::promise, um in den gemeinsam genutzten Status zu schreiben.

David Rodríguez - Dribeas
quelle
12
@KerrekSB: std::asyncKann konzeptionell (dies wird vom Standard nicht vorgeschrieben) als eine Funktion verstanden, die eine erstellt std::promise, diese in einen Thread-Pool schiebt (eine Art Thread-Pool, möglicherweise ein neuer Thread, ...) und zurückgibt die std::futuredem Anrufer zugeordnete. Auf der Client-Seite würden Sie auf der warten std::futureund ein Thread am anderen Ende würde das Ergebnis berechnen und in der speichern std::promise. Hinweis: Der Standard erfordert den gemeinsamen Status und std::futuredas Vorhandensein eines std::promisein diesem speziellen Anwendungsfall, jedoch nicht das Vorhandensein eines .
David Rodríguez - Dribeas
6
@KerrekSB: std::futureruft joinden Thread nicht auf, er hat einen Zeiger auf einen gemeinsam genutzten Status, der der eigentliche Kommunikationspuffer ist. Der gemeinsam genutzte Status verfügt über einen Synchronisationsmechanismus (wahrscheinlich std::function+ std::condition_variable, um den Aufrufer zu sperren, bis der std::promiseerfüllt ist. Die Ausführung des Threads ist orthogonal zu all dem, und in vielen Implementierungen werden Sie möglicherweise feststellen, dass diese std::asyncnicht von neuen Threads ausgeführt werden, die dann verbunden werden, aber eher durch einen Thread-Pool, dessen Lebensdauer sich bis zum Ende des Programms erstreckt.
David Rodríguez - Dribeas
1
@ DavidRodríguez-dribeas: Bitte bearbeiten Sie die Informationen aus den Kommentaren in Ihre Antwort.
Marc Mutz - mmutz
2
@JonathanWakely: Das bedeutet nicht, dass es in einem neuen Thread ausgeführt werden muss, sondern nur, dass es asynchron ausgeführt werden muss, als ob es in einem neu erstellten Thread ausgeführt worden wäre. Der Hauptvorteil von std::asyncist, dass die Laufzeitbibliothek die richtigen Entscheidungen für Sie in Bezug auf die Anzahl der zu erstellenden Threads treffen kann. In den meisten Fällen erwarte ich Laufzeiten, die Thread-Pools verwenden. Derzeit verwendet VS2012 einen Thread-Pool unter der Haube und verstößt nicht gegen die Als-ob- Regel. Beachten Sie, dass es sehr wenig Garantien sind , die für diese besondere erfüllt werden müssen , als-ob .
David Rodríguez - Dribeas
1
Thread-Einheimische müssen neu initialisiert werden, aber die Als-ob-Regel erlaubt alles (weshalb ich "als ob" kursiv schreibe :)
Jonathan Wakely
11

std::promiseist der Kanal oder Pfad für Informationen, die von der asynchronen Funktion zurückgegeben werden sollen. std::futureist der Synchronisationsmechanismus, der den Anrufer warten lässt, bis der im Wert enthaltene Rückgabewert std::promisebereit ist (dh sein Wert wird innerhalb der Funktion festgelegt).

kjp
quelle
8

Es gibt wirklich 3 Kernentitäten in der asynchronen Verarbeitung. C ++ 11 konzentriert sich derzeit auf zwei davon.

Die wichtigsten Dinge, die Sie benötigen, um eine Logik asynchron auszuführen, sind:

  1. Die Aufgabe (Logik, die als ein Funktorobjekt verpackt ist), die 'irgendwo' ausgeführt wird.
  2. Der eigentliche Verarbeitungsknoten - ein Thread, ein Prozess usw., der solche Funktoren ausführt, wenn sie ihm bereitgestellt werden. Sehen Sie sich das Entwurfsmuster "Befehl" an, um eine gute Vorstellung davon zu erhalten, wie ein grundlegender Worker-Thread-Pool dies tut.
  3. Das Ergebnis-Handle : Jemand benötigt dieses Ergebnis und benötigt ein Objekt, das es für ihn abruft. Aus OOP- und anderen Gründen sollte das Warten oder Synchronisieren in den APIs dieses Handles erfolgen.

C ++ 11 nennt die Dinge, von denen ich in (1) spreche std::promise, und die in (3) std::future. std::threadist das einzige, was öffentlich vorgesehen ist (2). Dies ist bedauerlich, da echte Programme Thread- und Speicherressourcen verwalten müssen und die meisten möchten, dass Aufgaben in Thread-Pools ausgeführt werden, anstatt für jede kleine Aufgabe einen Thread zu erstellen und zu zerstören (was fast immer zu unnötigen Leistungseinbußen führt und leicht Ressourcen erstellen kann Hunger, der noch schlimmer ist).

Laut Herb Sutter und anderen std::executorMitgliedern des C ++ 11 Brain Trust gibt es vorläufige Pläne, eine hinzuzufügen, die - ähnlich wie in Java - die Grundlage für Thread-Pools und logisch ähnliche Setups für (2) sein wird. Vielleicht werden wir es in C ++ 2014 sehen, aber meine Wette ähnelt eher C ++ 17 (und Gott helfe uns, wenn sie den Standard für diese verpfuschen).

Zack Yezek
quelle
7

A std::promisewird als Endpunkt für ein Versprechen / Zukunftspaar erstellt und das std::future(aus dem std :: Versprechen mit der get_future()Methode erstellte) ist der andere Endpunkt. Dies ist eine einfache One-Shot-Methode, mit der zwei Threads synchronisiert werden können, wenn ein Thread über eine Nachricht Daten an einen anderen Thread liefert.

Sie können sich vorstellen, dass ein Thread ein Versprechen zur Bereitstellung von Daten erstellt und der andere Thread das Versprechen für die Zukunft sammelt. Dieser Mechanismus kann nur einmal verwendet werden.

Der Versprechungs- / Zukunftsmechanismus ist nur eine Richtung, vom Thread, der die set_value()Methode von a verwendet, std::promisebis zum Thread, der die get()von a verwendet std::future, um die Daten zu empfangen. Eine Ausnahme wird generiert, wenn dieget() Methode einer Zukunft mehrmals aufgerufen wird.

Wenn der Faden mit der std::promisenicht benutzt hat set_value()sein Versprechen dann zu erfüllen , wenn die zweiten Thread Anrufe get()von dem std::futuredas Versprechen zu sammeln, wird der zweite Thread in einen Wartezustand gehen , bis das Versprechen durch den ersten Thread mit der erfüllt ist , std::promisewenn es die verwendete set_value()Methode um die Daten zu senden.

Mit den vorgeschlagenen Coroutinen der technischen Spezifikation N4663 Programmiersprachen - C ++ - Erweiterungen für Coroutinen und der Unterstützung des Visual Studio 2017 C ++ - Compilers von co_awaitist es auch möglich , Coroutinenfunktionen zu verwenden std::futureund std::asynczu schreiben. Weitere Informationen finden Sie in der Diskussion und im Beispiel unter https://stackoverflow.com/a/50753040/1466970. In diesem Abschnitt wird die Verwendung von std::futurewith erläutert co_await.

Der folgende Beispielcode, eine einfache Visual Studio 2013 Windows-Konsolenanwendung, zeigt die Verwendung einiger C ++ 11-Parallelitätsklassen / -vorlagen und anderer Funktionen. Es zeigt eine Verwendung für Versprechen / Zukunft, die gut funktioniert, autonome Threads, die einige Aufgaben erledigen und anhalten, und eine Verwendung, bei der ein synchroneres Verhalten erforderlich ist und das Versprechen / Zukunftspaar aufgrund der Notwendigkeit mehrerer Benachrichtigungen nicht funktioniert.

Ein Hinweis zu diesem Beispiel sind die Verzögerungen, die an verschiedenen Stellen hinzugefügt wurden. Diese Verzögerungen wurden nur hinzugefügt, um sicherzustellen, dass die verschiedenen Nachrichten, die mit auf die Konsole gedruckt std::coutwerden, klar sind und der Text aus den verschiedenen Threads nicht miteinander vermischt wird.

Der erste Teil von main()besteht darin, drei zusätzliche Threads zu erstellen std::promiseund std::futureDaten zwischen den Threads zu verwenden und zu senden. Ein interessanter Punkt ist, wo der Hauptthread einen Thread, T2, startet, der auf Daten vom Hauptthread wartet, etwas tut und dann Daten an den dritten Thread, T3, sendet, der dann etwas tut und Daten zurück an den sendet Haupt-Bedroung.

Der zweite Teil von main()erstellt zwei Threads und eine Reihe von Warteschlangen, um mehrere Nachrichten vom Hauptthread an jeden der beiden erstellten Threads zuzulassen. Wir können nicht verwenden std::promiseund std::futuredafür, weil das Versprechen / zukünftige Duo ein Schuss ist und nicht wiederholt verwendet werden kann.

Die Quelle für die Klasse Sync_queuestammt aus Stroustrups The C ++ Programming Language: 4th Edition.

// cpp_threads.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include <thread>  // std::thread is defined here
#include <future>  // std::future and std::promise defined here

#include <list>    // std::list which we use to build a message queue on.

static std::atomic<int> kount(1);       // this variable is used to provide an identifier for each thread started.

//------------------------------------------------
// create a simple queue to let us send notifications to some of our threads.
// a future and promise are one shot type of notifications.
// we use Sync_queue<> to have a queue between a producer thread and a consumer thread.
// this code taken from chapter 42 section 42.3.4
//   The C++ Programming Language, 4th Edition by Bjarne Stroustrup
//   copyright 2014 by Pearson Education, Inc.
template<typename Ttype>
class Sync_queue {
public:
    void  put(const Ttype &val);
    void  get(Ttype &val);

private:
    std::mutex mtx;                   // mutex used to synchronize queue access
    std::condition_variable cond;     // used for notifications when things are added to queue
    std::list <Ttype> q;              // list that is used as a message queue
};

template<typename Ttype>
void Sync_queue<Ttype>::put(const Ttype &val) {
    std::lock_guard <std::mutex> lck(mtx);
    q.push_back(val);
    cond.notify_one();
}

template<typename Ttype>
void Sync_queue<Ttype>::get(Ttype &val) {
    std::unique_lock<std::mutex> lck(mtx);
    cond.wait(lck, [this]{return  !q.empty(); });
    val = q.front();
    q.pop_front();
}
//------------------------------------------------


// thread function that starts up and gets its identifier and then
// waits for a promise to be filled by some other thread.
void func(std::promise<int> &jj) {
    int myId = std::atomic_fetch_add(&kount, 1);   // get my identifier
    std::future<int> intFuture(jj.get_future());
    auto ll = intFuture.get();   // wait for the promise attached to the future
    std::cout << "  func " << myId << " future " << ll << std::endl;
}

// function takes a promise from one thread and creates a value to provide as a promise to another thread.
void func2(std::promise<int> &jj, std::promise<int>&pp) {
    int myId = std::atomic_fetch_add(&kount, 1);   // get my identifier
    std::future<int> intFuture(jj.get_future());
    auto ll = intFuture.get();     // wait for the promise attached to the future

    auto promiseValue = ll * 100;   // create the value to provide as promised to the next thread in the chain
    pp.set_value(promiseValue);
    std::cout << "  func2 " << myId << " promised " << promiseValue << " ll was " << ll << std::endl;
}

// thread function that starts up and waits for a series of notifications for work to do.
void func3(Sync_queue<int> &q, int iBegin, int iEnd, int *pInts) {
    int myId = std::atomic_fetch_add(&kount, 1);

    int ll;
    q.get(ll);    // wait on a notification and when we get it, processes it.
    while (ll > 0) {
        std::cout << "  func3 " << myId << " start loop base " << ll << " " << iBegin << " to " << iEnd << std::endl;
        for (int i = iBegin; i < iEnd; i++) {
            pInts[i] = ll + i;
        }
        q.get(ll);  // we finished this job so now wait for the next one.
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::chrono::milliseconds myDur(1000);

    // create our various promise and future objects which we are going to use to synchronise our threads
    // create our three threads which are going to do some simple things.
    std::cout << "MAIN #1 - create our threads." << std::endl;

    // thread T1 is going to wait on a promised int
    std::promise<int> intPromiseT1;
    std::thread t1(func, std::ref(intPromiseT1));

    // thread T2 is going to wait on a promised int and then provide a promised int to thread T3
    std::promise<int> intPromiseT2;
    std::promise<int> intPromiseT3;

    std::thread t2(func2, std::ref(intPromiseT2), std::ref(intPromiseT3));

    // thread T3 is going to wait on a promised int and then provide a promised int to thread Main
    std::promise<int> intPromiseMain;
    std::thread t3(func2, std::ref(intPromiseT3), std::ref(intPromiseMain));

    std::this_thread::sleep_for(myDur);
    std::cout << "MAIN #2 - provide the value for promise #1" << std::endl;
    intPromiseT1.set_value(22);

    std::this_thread::sleep_for(myDur);
    std::cout << "MAIN #2.2 - provide the value for promise #2" << std::endl;
    std::this_thread::sleep_for(myDur);
    intPromiseT2.set_value(1001);
    std::this_thread::sleep_for(myDur);
    std::cout << "MAIN #2.4 - set_value 1001 completed." << std::endl;

    std::future<int> intFutureMain(intPromiseMain.get_future());
    auto t3Promised = intFutureMain.get();
    std::cout << "MAIN #2.3 - intFutureMain.get() from T3. " << t3Promised << std::endl;

    t1.join();
    t2.join();
    t3.join();

    int iArray[100];

    Sync_queue<int> q1;    // notification queue for messages to thread t11
    Sync_queue<int> q2;    // notification queue for messages to thread t12

    std::thread t11(func3, std::ref(q1), 0, 5, iArray);     // start thread t11 with its queue and section of the array
    std::this_thread::sleep_for(myDur);
    std::thread t12(func3, std::ref(q2), 10, 15, iArray);   // start thread t12 with its queue and section of the array
    std::this_thread::sleep_for(myDur);

    // send a series of jobs to our threads by sending notification to each thread's queue.
    for (int i = 0; i < 5; i++) {
        std::cout << "MAIN #11 Loop to do array " << i << std::endl;
        std::this_thread::sleep_for(myDur);  // sleep a moment for I/O to complete
        q1.put(i + 100);
        std::this_thread::sleep_for(myDur);  // sleep a moment for I/O to complete
        q2.put(i + 1000);
        std::this_thread::sleep_for(myDur);  // sleep a moment for I/O to complete
    }

    // close down the job threads so that we can quit.
    q1.put(-1);    // indicate we are done with agreed upon out of range data value
    q2.put(-1);    // indicate we are done with agreed upon out of range data value

    t11.join();
    t12.join();
    return 0;
}

Diese einfache Anwendung erstellt die folgende Ausgabe.

MAIN #1 - create our threads.
MAIN #2 - provide the value for promise #1
  func 1 future 22
MAIN #2.2 - provide the value for promise #2
  func2 2 promised 100100 ll was 1001
  func2 3 promised 10010000 ll was 100100
MAIN #2.4 - set_value 1001 completed.
MAIN #2.3 - intFutureMain.get() from T3. 10010000
MAIN #11 Loop to do array 0
  func3 4 start loop base 100 0 to 5
  func3 5 start loop base 1000 10 to 15
MAIN #11 Loop to do array 1
  func3 4 start loop base 101 0 to 5
  func3 5 start loop base 1001 10 to 15
MAIN #11 Loop to do array 2
  func3 4 start loop base 102 0 to 5
  func3 5 start loop base 1002 10 to 15
MAIN #11 Loop to do array 3
  func3 4 start loop base 103 0 to 5
  func3 5 start loop base 1003 10 to 15
MAIN #11 Loop to do array 4
  func3 4 start loop base 104 0 to 5
  func3 5 start loop base 1004 10 to 15
Richard Chambers
quelle
1

Das Versprechen ist das andere Ende des Drahtes.

Stellen Sie sich vor, Sie müssen den Wert eines von einem futureberechneten Wesens abrufen async. Sie möchten jedoch nicht, dass es im selben Thread berechnet wird, und Sie erzeugen nicht einmal "jetzt" einen Thread - möglicherweise wurde Ihre Software so konzipiert, dass ein Thread aus einem Pool ausgewählt wird, sodass Sie nicht wissen, wer dies tun wird Führen Sie am Ende die Berechnung durch.

Was übergeben Sie nun an diesen (noch unbekannten) Thread / Klasse / Entität? Sie bestehen das nicht future, da dies das Ergebnis ist . Sie möchten etwas übergeben, das mit dem verbunden ist futureund das andere Ende des Drahtes darstellt , also fragen Sie einfach das ab, futureohne zu wissen, wer tatsächlich etwas berechnet / schreibt.

Das ist der promise. Es ist ein Griff, der mit Ihrem verbunden ist future. Wenn futurees sich um einen Lautsprecher handelt und get()Sie zuhören, bis ein Ton ertönt, promisehandelt es sich um ein Mikrofon . Aber nicht irgendein Mikrofon, sondern das Mikrofon, das mit einem einzigen Draht an den von Ihnen gehaltenen Lautsprecher angeschlossen ist. Sie wissen vielleicht, wer am anderen Ende ist, aber Sie müssen es nicht wissen - Sie geben es einfach und warten, bis die andere Partei etwas sagt.

Narcolessico
quelle
0

http://www.cplusplus.com/reference/future/promise/

Erklärung eines Satzes: furture :: get () wartet für immer auf promse :: set_value ().

void print_int(std::future<int>& fut) {
    int x = fut.get(); // future would wait prom.set_value forever
    std::cout << "value: " << x << '\n';
}

int main()
{
    std::promise<int> prom;                      // create promise

    std::future<int> fut = prom.get_future();    // engagement with future

    std::thread th1(print_int, std::ref(fut));  // send future to new thread

    prom.set_value(10);                         // fulfill promise
                                                 // (synchronizes with getting the future)
    th1.join();
    return 0;
}
Zhang
quelle