Co to jest std :: obietnica?

384

Jestem dość zaznajomieni z C ++ 11-tych std::thread, std::asynca std::futureskładniki (np zobaczyć tę odpowiedź ), które są prosto do przodu.

Nie mogę jednak do końca zrozumieć, co to std::promisejest, co robi i w jakich sytuacjach najlepiej go wykorzystać. Sam standardowy dokument nie zawiera wielu informacji poza streszczeniem klas, podobnie jak :: :: thread .

Czy ktoś mógłby podać krótki, zwięzły przykład sytuacji, w której jest std::promiseto potrzebne i gdzie jest to najbardziej idiomatyczne rozwiązanie?

Kerrek SB
źródło
2
Oto kod z tym w: en.cppreference.com/w/cpp/thread/future
Chris
58
Naprawdę, bardzo krótka wersja brzmi: std::promiseskąd std::futurepochodzi. std::futurepozwala odzyskać obiecaną wartość . Kiedy wołasz get()o przyszłość, czeka ona na właściciela, std::promisez którym ustala wartość (przez wywołanie set_valueobietnicy). Jeśli obietnica zostanie zniszczona przed ustaleniem wartości, a następnie wezwiesz get()przyszłość związaną z tą obietnicą, otrzymasz std::broken_promisewyjątek, ponieważ obiecano ci wartość, ale nie możesz jej zdobyć.
James McNellis,
14
Polecam, jeśli możesz / chcesz,
rzucić
32
@KerrekSB std::broken_promiseto najlepiej nazwany identyfikator w standardowej bibliotece. I nie ma std::atomic_future.
Cubbi
3
Downvoter, chcesz wyjaśnić swój sprzeciw?
Kerrek SB,

Odpowiedzi:

182

Innymi słowy [futures.state] a std::futureto asynchroniczny obiekt zwrotny („obiekt, który odczytuje wyniki ze stanu wspólnego”), a a std::promisejest asynchronicznym dostawcą („obiekt, który zapewnia wynik do stanu wspólnego”), tj. obietnica to rzecz, na której ustawiasz wynik, abyś mógł uzyskać go z powiązanej przyszłości.

Dostawca asynchroniczny jest tym, co początkowo tworzy stan współdzielony, do którego odnosi się przyszłość. std::promisejest jednym rodzajem dostawcy asynchronicznego, std::packaged_taskinnym jest, a wewnętrzny szczegół std::asyncinnego. Każdy z nich może utworzyć stan wspólny i dać cistd::future udział, który współużytkuje ten stan, i może go przygotować.

std::asyncto narzędzie wygody wyższego poziomu, które zapewnia asynchroniczny obiekt wyniku i wewnętrznie zajmuje się tworzeniem dostawcy asynchronicznego i przygotowaniem stanu współużytkowanego po zakończeniu zadania. Możesz emulować go za pomocą std::packaged_task(lub std::binda std::promise) i std::threadale jest to bezpieczniejsze i łatwiejsze w użyciu std::async.

std::promisejest nieco niższy poziom, ponieważ gdy chcesz przekazać wynik asynchroniczny w przyszłość, ale kodu, który przygotowuje wynik, nie można zawrzeć w jednej funkcji odpowiedniej do przekazania std::async. Na przykład możesz mieć tablicę kilku promisesi powiązanych z nimi futurei mieć jeden wątek, który wykonuje kilka obliczeń i ustawia wynik dla każdej obietnicy. asyncpozwoli ci zwrócić tylko jeden wynik, aby zwrócić kilka, musisz zadzwonić asynckilka razy, co może zmarnować zasoby.

Jonathan Wakely
źródło
10
Może marnować zasoby? Może być niepoprawny, jeśli ten kod nie może być zrównoleglony.
Szczeniak
„powrót asynchroniczny” i „odczyt wyniku ze stanu wspólnego” są w większości ortogonalne, co sprawia, że ​​pierwsze zdanie jest nieco mylące. Czy chcesz powiedzieć, że podział państwa jest między przyszłością a obietnicą? Jeśli tak, to powiedz to od samego początku.
einpoklum
@einpoklum dlaczego przestałeś czytać „asynchroniczny obiekt zwrotny” przed ostatnim słowem? Cytuję terminologię standardu. A futurejest konkretnym przykładem asynchronicznego obiektu zwracającego , który jest obiektem, który odczytuje wynik, który został zwrócony asynchronicznie, poprzez stan współdzielony. A promisejest konkretnym przykładem dostawcy asynchronicznego , który jest obiektem, który zapisuje wartość do współużytkowanego stanu, który można odczytać asynchronicznie. Miałem na myśli to, co napisałem.
Jonathan Wakely
496

Rozumiem teraz sytuację nieco lepiej (w niemałej ilości z powodu odpowiedzi tutaj!), Więc pomyślałem, że dodam trochę własnego opisu.


W C ++ 11 istnieją dwie różne, choć powiązane, koncepcje: obliczenia asynchroniczne (funkcja wywoływana gdzie indziej) i współbieżne wykonywanie ( wątek , coś, co działa jednocześnie). Te dwa są nieco ortogonalnymi pojęciami. Obliczenia asynchroniczne to po prostu inny smak wywołania funkcji, podczas gdy wątek jest kontekstem wykonania. Wątki są przydatne same w sobie, ale dla celów tej dyskusji potraktuję je jako szczegół implementacyjny.


Istnieje hierarchia abstrakcji dla obliczeń asynchronicznych. Na przykład, załóżmy, że mamy funkcję, która przyjmuje pewne argumenty:

int foo(double, char, bool);

Po pierwsze mamy szablon std::future<T>, który reprezentuje przyszłą wartość typu T. Wartość można pobrać za pomocą funkcji składowej get(), która skutecznie synchronizuje program, czekając na wynik. Alternatywnie, przyszłe wsparcie wait_for(), które można wykorzystać do zbadania, czy wynik jest już dostępny. Futures należy traktować jako asynchroniczny zamiennik zwykłych typów zwrotu. Dla naszej przykładowej funkcji oczekujemystd::future<int> .

Przejdźmy teraz do hierarchii, od najwyższego do najniższego poziomu:

  1. std::async: Najwygodniejszym i najprostszym sposobem wykonywania obliczeń asynchronicznych jest użycie asyncszablonu funkcji, który natychmiast zwraca pasującą przyszłość:

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

    Mamy bardzo niewielką kontrolę nad szczegółami. W szczególności nie wiemy nawet, czy funkcja jest wykonywana równocześnie, szeregowo get(), czy przez jakąś inną czarną magię. Jednak wynik można łatwo uzyskać w razie potrzeby:

    auto res = fut.get();  // is an int
  2. Możemy teraz zastanowić się, jak wdrożyć coś takiego async, ale w sposób, który kontrolujemy. Na przykład możemy nalegać, aby funkcja była wykonywana w osobnym wątku. Wiemy już, że możemy zapewnić osobny wątek za pomocą std::threadklasy.

    Następnego niższy poziom abstrakcji robi dokładnie to: std::packaged_task. Jest to szablon, który otacza funkcję i zapewnia przyszłość dla wartości zwracanej przez funkcje, ale sam obiekt można wywołać, a wywołanie go według uznania użytkownika. Możemy to ustawić w następujący sposób:

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

    Przyszłość staje się gotowa, gdy zadzwonimy do zadania i połączenie zostanie zakończone. To idealna praca dla osobnego wątku. Musimy tylko przenieść zadanie do wątku:

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

    Wątek zaczyna działać natychmiast. Możemy detachto albo mieć joinna końcu zakresu, albo kiedykolwiek (np. Używając scoped_threadopakowania Anthony'ego Williamsa , który naprawdę powinien znajdować się w standardowej bibliotece). Jednak szczegóły korzystania std::threadnie dotyczą nas tutaj; po prostu pamiętaj, aby w thrkońcu dołączyć lub odłączyć . Liczy się to, że po zakończeniu wywołania funkcji nasz wynik jest gotowy:

    auto res = fut.get();  // as before
  3. Teraz jesteśmy na najniższym poziomie: jak moglibyśmy zrealizować spakowane zadanie? Tu właśnie std::promisenadchodzi. Obietnica jest budulcem komunikacji z przyszłością. Główne kroki to:

    • Wątek wywołujący stanowi obietnicę.

    • Wątek wywołujący otrzymuje przyszłość z obietnicy.

    • Obietnica wraz z argumentami funkcji są przenoszone do osobnego wątku.

    • Nowy wątek wykonuje funkcję i spełnia obietnicę.

    • Oryginalny wątek pobiera wynik.

    Na przykład, oto nasze własne „spakowane zadanie”:

    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
    };

    Użycie tego szablonu jest zasadniczo takie samo jak w przypadku std::packaged_task. Pamiętaj, że przeniesienie całego zadania powoduje przeniesienie obietnicy. W sytuacjach bardziej doraźnych można również jawnie przenieść obiekt obietnicy do nowego wątku i uczynić z niego argument funkcji funkcji wątku, ale opakowanie zadania takie jak powyższe wydaje się bardziej elastycznym i mniej ingerującym rozwiązaniem.


Robienie wyjątków

Obietnice są ściśle związane z wyjątkami. Sam interfejs obietnicy nie wystarcza do pełnego przekazania jej stanu, więc wyjątki są zgłaszane, ilekroć operacja na obietnicy nie ma sensu. Wszystkie wyjątki są tego samego rodzaju std::future_error, co wynika z std::logic_error. Po pierwsze, opis niektórych ograniczeń:

  • Domyślnie skonstruowana obietnica jest nieaktywna. Nieaktywne obietnice mogą umrzeć bez konsekwencji.

  • Obietnica staje się aktywna po uzyskaniu przyszłości za pośrednictwem get_future(). Można jednak uzyskać tylko jedną przyszłość!

  • Obietnica musi zostać spełniona albo musi zostać set_value()ustalona wyjątek set_exception()przed końcem jej życia, jeśli jego przyszłość ma zostać wykorzystana. Spełniona obietnica może umrzeć bez konsekwencji i stanie get()się dostępna w przyszłości. Obietnica z wyjątkiem podniesie przechowywany wyjątek po wywołaniu get()w przyszłości. Jeśli obietnica umrze bez żadnej wartości ani wyjątku, przywołanie get()przyszłości spowoduje powstanie wyjątku „złamana obietnica”.

Oto mała seria testów, aby wykazać te różne wyjątkowe zachowania. Po pierwsze, uprząż:

#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;
    }
}

Teraz przejdźmy do testów.

Przypadek 1: nieaktywna obietnica

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

Przypadek 2: aktywna obietnica, niewykorzystana

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

Przypadek 3: Zbyt wiele kontraktów terminowych

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

Przypadek 4: spełniona obietnica

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

Przypadek 5: Zbyt duża satysfakcja

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();
}

To samo jest wyjątek, jeżeli istnieje więcej niż jeden z albo z set_valuei set_exception.

Przypadek 6: wyjątek

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

Przypadek 7: Złamana obietnica

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
źródło
Powiedziałeś „... który skutecznie synchronizuje program, czekając na wynik”. . Co oznacza tutaj „synchronizacja”? Co oznacza całe oświadczenie? Nie jestem w stanie tego zrozumieć. Żadne znaczenie słowa „synchronizuj” z tego wpisu słownika nie pomaga mi zrozumieć zdania. Czy samo „czekanie” oznacza „synchronizację”? Czy każde oczekiwanie synchronizuje się? Myślę, że częściowo zrozumieć, co to znaczy, ale nie jestem pewien, co rzeczywiście myśli.
Nawaz
9
Dobra odpowiedź, dziękuję za twoją pomoc. O części std :: async pamiętam, że moglibyśmy ustalić, że odrodzi się kolejny wątek lub będzie działał synchronicznie z flagą (std :: launch :: async, std :: launch :: deferred)
StereoMatching 12.10.13
1
@FelixDombek: Idealne przekazywanie itd. std::functionMa wielu konstruktorów; nie ma powodu, aby nie ujawniać ich konsumentowi my_task.
Kerrek SB
1
@DaveedV .: Dziękujemy za opinię! Tak, to przypadek testowy 7: jeśli zniszczysz obietnicę bez ustawiania wartości lub wyjątku, wówczas przywołanie get()przyszłości spowoduje powstanie wyjątku. Wyjaśnię to, dodając „zanim zostanie zniszczone”; proszę dać mi znać, jeśli jest to wystarczająco jasne.
Kerrek SB
3
Wreszcie got()mój futureod grokking bibliotekę pomocy gwintu na promiseswojej niesamowitej wyjaśnienia!
słoneczny księżyc
33

Dobry opis pisze Bartosz Milewski .

C ++ dzieli implementację kontraktów terminowych na zestaw małych bloków

std :: promise jest jedną z tych części.

Obietnica jest narzędziem przekazywania wartości zwracanej (lub wyjątku) z wątku wykonującego funkcję do wątku, który pojawi się w przyszłości funkcji.

...

Przyszłością jest obiekt synchronizacji zbudowany wokół odbiorczego końca kanału obietnicy.

Tak więc, jeśli chcesz skorzystać z przyszłości, otrzymujesz obietnicę, której używasz, aby uzyskać wynik przetwarzania asynchronicznego.

Przykład ze strony to:

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
źródło
4
Widok obietnicy konstruktora nici w końcu sprawił, że grosz spadł. Artykuł Bartosza może nie jest najlepszy, ale wyjaśnia, w jaki sposób elementy łączą się ze sobą. Dzięki.
Kerrek SB
28

W przybliżeniu można to potraktować std::promisejako drugi koniec std::future(jest to fałsz , ale dla ilustracji można myśleć tak, jakby tak było). Konsumencki koniec kanału komunikacyjnego użyłby a std::futuredo zużywania danych ze stanu wspólnego, podczas gdy wątek producenta użyłby a std::promisedo zapisu do stanu wspólnego.

David Rodríguez - dribeas
źródło
12
@KerrekSB: std::asyncmoże koncepcyjnie (nie jest to wymagane przez standard) rozumiane jako funkcja, która tworzy a std::promise, wypycha to do puli wątków (swego rodzaju, może być pulą wątków, może być nowym wątkiem, ...) i zwraca powiązany std::futurez dzwoniącym. Po stronie klienta czekasz na, std::futurea wątek na drugim końcu oblicza wynik i zapisuje go w pliku std::promise. Uwaga: średnia wymaga stan współdzieloną i tym std::future, ale nie o istnieniu std::promisew tym konkretnym przypadku zastosowania.
David Rodríguez - dribeas
6
@KerrekSB: std::futurenie będzie wywoływał joinwątku, ma wskaźnik do stanu wspólnego, który jest rzeczywistym buforem komunikacyjnym. Stan współdzielony ma mechanizm synchronizacji (prawdopodobnie std::function+, std::condition_variableaby zablokować program wywołujący, dopóki nie std::promisezostanie spełniony. Wykonanie wątku jest do tego wszystkiego ortogonalne, aw wielu implementacjach może się okazać, że std::asyncnie są wykonywane przez nowe wątki, które są następnie łączone, ale raczej przez pulę wątków, której żywotność przedłuża się do końca programu
David Rodríguez - dribeas
1
@ DavidRodríguez-dribeas: edytuj informacje zawarte w komentarzach w swojej odpowiedzi.
Marc Mutz - mmutz
2
@JathanathanWakely: Nie oznacza to, że należy go wykonać w nowym wątku, a jedynie, że należy go wykonać asynchronicznie tak, jakby działał w nowo utworzonym wątku. Główną zaletą std::asyncjest to, że biblioteka środowiska wykonawczego może podejmować właściwe decyzje w odniesieniu do liczby tworzonych wątków, aw większości przypadków oczekuję środowisk wykonawczych korzystających z pul wątków. Obecnie VS2012 korzysta z puli wątków pod maską i nie narusza reguły as-if . Należy pamiętać, że istnieje bardzo małe gwarancje, które muszą być spełnione, aby ten szczególny as-czy .
David Rodríguez - dribeas
1
Lokalni użytkownicy wątków muszą zostać ponownie zainicjowani, ale zasada „jak gdyby” pozwala na wszystko (dlatego kursywą
wpisuję
11

std::promisejest kanałem lub ścieżką do zwracania informacji z funkcji asynchronicznej. std::futurejest mechanizmem synchronizacji, który powoduje, że osoba dzwoniąca czeka, aż wartość zwracana w obiekcie std::promisejest gotowa (co oznacza, że ​​jej wartość jest ustawiana w funkcji).

kjp
źródło
8

W przetwarzaniu asynchronicznym są naprawdę 3 podstawowe jednostki. C ++ 11 koncentruje się obecnie na 2 z nich.

Podstawowe rzeczy, których potrzebujesz, aby uruchomić logikę asynchronicznie, to:

  1. zadanie (logika pakowane jako jakiegoś przedmiotu funktora), która będzie działać „gdzieś”.
  2. Rzeczywisty węzeł przetwarzania - wątku, proces itp, który działa tak funktory gdy są one przewidziane do niego. Spójrz na wzorzec projektowy „Command”, aby dowiedzieć się, jak robi to podstawowa pula wątków roboczych.
  3. Uchwyt wynik : Ktoś musi ten wynik i potrzebuje obiektu, który dostanie się do nich. Z powodu OOP i innych powodów wszelkie operacje oczekiwania lub synchronizacji powinny być wykonywane w interfejsach API tego uchwytu.

C ++ 11 nazywa rzeczy, o których mówię w (1) std::promisei te w (3) std::future. std::threadjest jedyną rzeczą udostępnioną publicznie dla (2). Jest to niefortunne, ponieważ prawdziwe programy muszą zarządzać zasobami wątków i pamięci, a większość będzie chciała, aby zadania działały na pulach wątków zamiast tworzyć i niszczyć wątek dla każdego małego zadania (co prawie zawsze powoduje sam w sobie niepotrzebny wzrost wydajności i może łatwo tworzyć zasoby głód jest jeszcze gorszy).

Według Herb Sutter i innych osób z zaufania do C ++ 11 istnieją wstępne plany dodania, std::executorże - podobnie jak w Javie - będzie podstawą dla pul wątków i logicznie podobnych konfiguracji dla (2). Być może zobaczymy to w C ++ 2014, ale mój zakład bardziej przypomina C ++ 17 (i Boże, pomóż nam, jeśli skończą na tym standard).

Zack Yezek
źródło
7

A std::promisejest tworzony jako punkt końcowy dla pary obietnica / przyszłość, a std::future(utworzony z std :: promise przy użyciuget_future() metody) jest drugim punktem końcowym. Jest to prosta metoda umożliwiająca synchronizację dwóch wątków, ponieważ jeden wątek dostarcza dane do drugiego wątku za pośrednictwem wiadomości.

Możesz myśleć o tym, jak jeden wątek tworzy obietnicę dostarczenia danych, a drugi wątek zbiera obietnicę w przyszłości. Tego mechanizmu można użyć tylko raz.

Mechanizm obietnica / przyszłość jest tylko jednym kierunkiem, od wątku, który używa set_value()metody a, std::promisedo wątku, który używa get()a std::futuredo otrzymania danych. Wyjątek jest generowany, jeśliget() metoda przyszłości zostanie wywołana więcej niż jeden raz.

Jeśli wątek z std::promisenie wykorzystał set_value()spełnić swoją obietnicę wtedy gdy drugie połączenia gwintowe get()z std::futurezebrać obietnicę, druga nitka przejdzie w stan oczekiwania, aż obietnica jest spełniona przez pierwszy wątek z std::promisekiedy używa set_value()metody wysłać dane.

Dzięki zaproponowanym coroutines specyfikacji technicznej N4663 Języki programowania - rozszerzenia C ++ dla Coroutines i obsłudze kompilatora Visual Studio 2017 C ++ co_await, możliwe jest także używanie std::futurei std::asyncpisanie funkcjonalności coroutine. Zobacz dyskusję i przykład w https://stackoverflow.com/a/50753040/1466970, który ma jedną sekcję omawiającą użycie std::futurez co_await.

Poniższy przykładowy kod, prosta aplikacja konsoli Windows dla programu Visual Studio 2013, pokazuje użycie kilku klas / szablonów współbieżności C ++ 11 i innych funkcji. Ilustruje zastosowanie dobrze działającej obietnicy / przyszłości, autonomicznych wątków, które wykonają pewne zadania i zatrzymanie, oraz zastosowania, w którym wymagane jest bardziej synchroniczne zachowanie, a ze względu na potrzebę wielu powiadomień para obietnica / przyszła nie działa.

Jedna uwaga na temat tego przykładu to opóźnienia dodane w różnych miejscach. Opóźnienia te zostały dodane tylko po to, aby upewnić się, że różne komunikaty drukowane przy użyciu konsoli std::coutbędą wyraźne, a tekst z kilku wątków nie będzie mieszany.

Pierwsza część main()polega na utworzeniu trzech dodatkowych wątków oraz wykorzystaniu std::promisei std::futurewysłaniu danych między wątkami. Ciekawym punktem jest to, że główny wątek uruchamia wątek T2, który będzie czekał na dane z głównego wątku, zrobi coś, a następnie wyśle ​​dane do trzeciego wątku, T3, który następnie coś zrobi i wyśle ​​dane z powrotem do główny wątek.

Druga część main()tworzy dwa wątki i zestaw kolejek, aby umożliwić wiele wiadomości z głównego wątku do każdego z dwóch utworzonych wątków. Nie możemy użyć std::promisei std::futuredo tego, ponieważ obietnica / przyszłość duet to jeden strzał i nie może być użyty wielokrotnie.

Źródłem klasy Sync_queuejest Stroustrup's 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;
}

Ta prosta aplikacja tworzy następujące dane wyjściowe.

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
źródło
1

Obietnica jest drugim końcem drutu.

Wyobraź sobie, że musisz odzyskać wartość futurebytu obliczoną przez async. Jednak nie chcesz, aby był on obliczany w tym samym wątku i nie odradzasz nawet wątku „teraz” - być może twoje oprogramowanie zostało zaprojektowane do wybierania wątku z puli, więc nie wiesz, kto to zrobi w końcu wykonaj obliczenia.

Co teraz przekazujesz do tego (jeszcze nieznanego) wątku / klasy / bytu? Nie zdajesz future, ponieważ taki jest wynik . Chcesz przekazać coś, co jest podłączone do futurei które reprezentuje drugi koniec drutu , więc po prostu zapytasz futureo to, nie wiedząc, kto tak naprawdę coś obliczy / napisze.

To jest promise. Jest to uchwyt podłączony do twojego future. Jeśli futurejest to głośnik , a wraz z get()tobą zacznij słuchać, aż usłyszysz dźwięk, promiseoznacza to , że jest mikrofonem ; ale nie byle mikrofon, to mikrofon podłączony za pomocą jednego przewodu do głośnika posiadasz. Możesz wiedzieć, kto jest na drugim końcu, ale nie musisz tego wiedzieć - po prostu daj to i poczekaj, aż druga strona coś powie.

Narcolessico
źródło
0

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

Wyjaśnienie jednego zdania: furture :: get () na zawsze czeka na obietnicę :: 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
źródło