Jestem dość zaznajomieni z C ++ 11-tych std::thread
, std::async
a std::future
składniki (np zobaczyć tę odpowiedź ), które są prosto do przodu.
Nie mogę jednak do końca zrozumieć, co to std::promise
jest, 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::promise
to potrzebne i gdzie jest to najbardziej idiomatyczne rozwiązanie?
c++
multithreading
c++11
promise
standard-library
Kerrek SB
źródło
źródło
std::promise
skądstd::future
pochodzi.std::future
pozwala odzyskać obiecaną wartość . Kiedy wołaszget()
o przyszłość, czeka ona na właściciela,std::promise
z którym ustala wartość (przez wywołanieset_value
obietnicy). Jeśli obietnica zostanie zniszczona przed ustaleniem wartości, a następnie wezwieszget()
przyszłość związaną z tą obietnicą, otrzymaszstd::broken_promise
wyjątek, ponieważ obiecano ci wartość, ale nie możesz jej zdobyć.std::broken_promise
to najlepiej nazwany identyfikator w standardowej bibliotece. I nie mastd::atomic_future
.Odpowiedzi:
Innymi słowy [futures.state] a
std::future
to asynchroniczny obiekt zwrotny („obiekt, który odczytuje wyniki ze stanu wspólnego”), a astd::promise
jest 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::promise
jest jednym rodzajem dostawcy asynchronicznego,std::packaged_task
innym jest, a wewnętrzny szczegółstd::async
innego. 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::async
to 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
(lubstd::bind
astd::promise
) istd::thread
ale jest to bezpieczniejsze i łatwiejsze w użyciustd::async
.std::promise
jest 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 przekazaniastd::async
. Na przykład możesz mieć tablicę kilkupromise
si powiązanych z nimifuture
i mieć jeden wątek, który wykonuje kilka obliczeń i ustawia wynik dla każdej obietnicy.async
pozwoli ci zwrócić tylko jeden wynik, aby zwrócić kilka, musisz zadzwonićasync
kilka razy, co może zmarnować zasoby.źródło
future
jest 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. Apromise
jest 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.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:
Po pierwsze mamy szablon
std::future<T>
, który reprezentuje przyszłą wartość typuT
. Wartość można pobrać za pomocą funkcji składowejget()
, która skutecznie synchronizuje program, czekając na wynik. Alternatywnie, przyszłe wsparciewait_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:
std::async
: Najwygodniejszym i najprostszym sposobem wykonywania obliczeń asynchronicznych jest użycieasync
szablonu funkcji, który natychmiast zwraca pasującą przyszłość: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: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::thread
klasy.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: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:
Wątek zaczyna działać natychmiast. Możemy
detach
to albo miećjoin
na końcu zakresu, albo kiedykolwiek (np. Używającscoped_thread
opakowania Anthony'ego Williamsa , który naprawdę powinien znajdować się w standardowej bibliotece). Jednak szczegóły korzystaniastd::thread
nie dotyczą nas tutaj; po prostu pamiętaj, aby wthr
końcu dołączyć lub odłączyć . Liczy się to, że po zakończeniu wywołania funkcji nasz wynik jest gotowy:Teraz jesteśmy na najniższym poziomie: jak moglibyśmy zrealizować spakowane zadanie? Tu właśnie
std::promise
nadchodzi. 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”:
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 zstd::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ątekset_exception()
przed końcem jej życia, jeśli jego przyszłość ma zostać wykorzystana. Spełniona obietnica może umrzeć bez konsekwencji i stanieget()
się dostępna w przyszłości. Obietnica z wyjątkiem podniesie przechowywany wyjątek po wywołaniuget()
w przyszłości. Jeśli obietnica umrze bez żadnej wartości ani wyjątku, przywołanieget()
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ąż:
Teraz przejdźmy do testów.
Przypadek 1: nieaktywna obietnica
Przypadek 2: aktywna obietnica, niewykorzystana
Przypadek 3: Zbyt wiele kontraktów terminowych
Przypadek 4: spełniona obietnica
Przypadek 5: Zbyt duża satysfakcja
To samo jest wyjątek, jeżeli istnieje więcej niż jeden z albo z
set_value
iset_exception
.Przypadek 6: wyjątek
Przypadek 7: Złamana obietnica
źródło
std::function
Ma wielu konstruktorów; nie ma powodu, aby nie ujawniać ich konsumentowimy_task
.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.got()
mójfuture
od grokking bibliotekę pomocy gwintu napromise
swojej niesamowitej wyjaśnienia!Dobry opis pisze Bartosz Milewski .
std :: promise jest jedną z tych części.
...
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:
źródło
W przybliżeniu można to potraktować
std::promise
jako drugi koniecstd::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 astd::future
do zużywania danych ze stanu wspólnego, podczas gdy wątek producenta użyłby astd::promise
do zapisu do stanu wspólnego.źródło
std::async
może koncepcyjnie (nie jest to wymagane przez standard) rozumiane jako funkcja, która tworzy astd::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ązanystd::future
z dzwoniącym. Po stronie klienta czekasz na,std::future
a wątek na drugim końcu oblicza wynik i zapisuje go w plikustd::promise
. Uwaga: średnia wymaga stan współdzieloną i tymstd::future
, ale nie o istnieniustd::promise
w tym konkretnym przypadku zastosowania.std::future
nie będzie wywoływałjoin
wątku, ma wskaźnik do stanu wspólnego, który jest rzeczywistym buforem komunikacyjnym. Stan współdzielony ma mechanizm synchronizacji (prawdopodobniestd::function
+,std::condition_variable
aby zablokować program wywołujący, dopóki niestd::promise
zostanie spełniony. Wykonanie wątku jest do tego wszystkiego ortogonalne, aw wielu implementacjach może się okazać, żestd::async
nie 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 programustd::async
jest 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 .std::promise
jest kanałem lub ścieżką do zwracania informacji z funkcji asynchronicznej.std::future
jest mechanizmem synchronizacji, który powoduje, że osoba dzwoniąca czeka, aż wartość zwracana w obiekciestd::promise
jest gotowa (co oznacza, że jej wartość jest ustawiana w funkcji).źródło
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:
C ++ 11 nazywa rzeczy, o których mówię w (1)
std::promise
i te w (3)std::future
.std::thread
jest 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).źródło
A
std::promise
jest tworzony jako punkt końcowy dla pary obietnica / przyszłość, astd::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::promise
do wątku, który używaget()
astd::future
do 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::promise
nie wykorzystałset_value()
spełnić swoją obietnicę wtedy gdy drugie połączenia gwintoweget()
zstd::future
zebrać obietnicę, druga nitka przejdzie w stan oczekiwania, aż obietnica jest spełniona przez pierwszy wątek zstd::promise
kiedy używaset_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żywaniestd::future
istd::async
pisanie funkcjonalności coroutine. Zobacz dyskusję i przykład w https://stackoverflow.com/a/50753040/1466970, który ma jedną sekcję omawiającą użyciestd::future
zco_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::cout
bę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 wykorzystaniustd::promise
istd::future
wysł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::promise
istd::future
do tego, ponieważ obietnica / przyszłość duet to jeden strzał i nie może być użyty wielokrotnie.Źródłem klasy
Sync_queue
jest Stroustrup's The C ++ Programming Language: 4th Edition.Ta prosta aplikacja tworzy następujące dane wyjściowe.
źródło
Obietnica jest drugim końcem drutu.
Wyobraź sobie, że musisz odzyskać wartość
future
bytu obliczoną przezasync
. 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 dofuture
i które reprezentuje drugi koniec drutu , więc po prostu zapytaszfuture
o to, nie wiedząc, kto tak naprawdę coś obliczy / napisze.To jest
promise
. Jest to uchwyt podłączony do twojegofuture
. Jeślifuture
jest to głośnik , a wraz zget()
tobą zacznij słuchać, aż usłyszysz dźwięk,promise
oznacza 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.źródło
http://www.cplusplus.com/reference/future/promise/
Wyjaśnienie jednego zdania: furture :: get () na zawsze czeka na obietnicę :: set_value ().
źródło