Capisco la situazione un po 'meglio ora (non poco per via delle risposte qui!), Quindi ho pensato di aggiungere un piccolo commento personale.
Esistono due concetti distinti, sebbene correlati, in C ++ 11: il calcolo asincrono (una funzione che viene chiamata altrove) e l'esecuzione simultanea (un thread , qualcosa che funziona contemporaneamente). I due sono concetti in qualche modo ortogonali. Il calcolo asincrono è solo un diverso sapore della chiamata di funzione, mentre un thread è un contesto di esecuzione. I thread sono utili a pieno titolo, ma ai fini di questa discussione, li tratterò come un dettaglio di implementazione.
Esiste una gerarchia di astrazione per il calcolo asincrono. Per esempio, supponiamo di avere una funzione che accetta alcuni argomenti:
int foo(double, char, bool);
Prima di tutto, abbiamo il modello std::future<T>
, che rappresenta un valore futuro di tipo T
. Il valore può essere recuperato tramite la funzione membro get()
, che sincronizza efficacemente il programma in attesa del risultato. In alternativa, un futuro supporta wait_for()
, che può essere utilizzato per sondare se il risultato è già disponibile. I futures dovrebbero essere considerati la sostituzione del drop-in asincrono per i tipi di rendimento ordinari. Per la nostra funzione di esempio, ci aspettiamo a std::future<int>
.
Ora, nella gerarchia, dal livello più alto al più basso:
std::async
: Il modo più conveniente e diretto per eseguire un calcolo asincrono è tramite async
modello di funzione, che restituisce immediatamente il futuro corrispondente:
auto fut = std::async(foo, 1.5, 'x', false); // is a std::future<int>
Abbiamo pochissimo controllo sui dettagli. In particolare, non sappiamo nemmeno se la funzione viene eseguita contemporaneamente, in serieget()
o da qualche altra magia nera. Tuttavia, il risultato è facilmente ottenibile quando necessario:
auto res = fut.get(); // is an int
Ora possiamo considerare come implementare qualcosa del genere async
, ma in un modo che noi controllare. Ad esempio, possiamo insistere sul fatto che la funzione sia eseguita in un thread separato. Sappiamo già che possiamo fornire un thread separato tramite la std::thread
classe.
Il prossimo livello inferiore di astrazione fa esattamente questo: std::packaged_task
. Questo è un modello che avvolge una funzione e fornisce un futuro per il valore restituito dalle funzioni, ma l'oggetto stesso è richiamabile e chiamarlo è a discrezione dell'utente. Possiamo configurarlo in questo modo:
std::packaged_task<int(double, char, bool)> tsk(foo);
auto fut = tsk.get_future(); // is a std::future<int>
Il futuro diventa pronto quando chiamiamo l'attività e la chiamata termina. Questo è il lavoro ideale per un thread separato. Dobbiamo solo assicurarcelo spostare l'attività nel thread:
std::thread thr(std::move(tsk), 1.5, 'x', false);
Il thread inizia a funzionare immediatamente. Possiamo detach
farlo o averlo join
alla fine dell'ambito o quando (ad es. Utilizzando il scoped_thread
wrapper di Anthony Williams , che dovrebbe essere nella libreria standard). I dettagli dell'uso std::thread
non ci riguardano qui, però; assicurati di unirti o staccarti thr
alla fine. Ciò che conta è che ogni volta che termina la chiamata di funzione, il nostro risultato è pronto:
auto res = fut.get(); // as before
Ora siamo al livello più basso: come implementeremo l'attività in pacchetto? È qui che std::promise
entra in gioco. La promessa è la base per comunicare con un futuro. I passaggi principali sono questi:
Il thread chiamante fa una promessa.
Il thread chiamante ottiene un futuro dalla promessa.
La promessa, insieme agli argomenti della funzione, vengono spostati in un thread separato.
Il nuovo thread esegue la funzione e mantiene la promessa.
Il thread originale recupera il risultato.
A titolo di esempio, ecco il nostro "task impacchettato":
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
};
L'uso di questo modello è essenzialmente lo stesso di quello di std::packaged_task
. Si noti che lo spostamento dell'intera attività comporta lo spostamento della promessa. In situazioni più ad hoc, si potrebbe anche spostare esplicitamente un oggetto promessa nel nuovo thread e renderlo un argomento di funzione della funzione thread, ma un wrapper di attività come quello sopra sembra una soluzione più flessibile e meno invadente.
Fare eccezioni
Le promesse sono intimamente correlate alle eccezioni. L'interfaccia di una promessa da sola non è sufficiente per comunicare completamente il suo stato, quindi vengono generate eccezioni ogni volta che un'operazione su una promessa non ha senso. Tutte le eccezioni sono di tipo std::future_error
, da cui deriva std::logic_error
. Prima di tutto, una descrizione di alcuni vincoli:
Una promessa costruita per impostazione predefinita è inattiva. Le promesse inattive possono morire senza conseguenze.
Una promessa diventa attiva quando si ottiene un futuro tramite get_future()
. Tuttavia, è possibile ottenere solo un futuro!
Una promessa deve essere soddisfatta tramite set_value()
o avere un'eccezione impostata set_exception()
prima della fine della sua vita se si vuole consumare il suo futuro. Una promessa soddisfatta può morire senza conseguenze e get()
diventa disponibile in futuro. Una promessa con un'eccezione solleverà l'eccezione memorizzata su chiamata del get()
futuro. Se la promessa muore senza valore né eccezione, la chiamata get()
al futuro solleverà un'eccezione "promessa non mantenuta".
Ecco una piccola serie di test per dimostrare questi vari comportamenti eccezionali. Innanzitutto, l'imbracatura:
#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;
}
}
Ora ai test.
Caso 1: promessa inattiva
int test()
{
std::promise<int> pr;
return 0;
}
// fine, no problems
Caso 2: promessa attiva, inutilizzata
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
return 0;
}
// fine, no problems; fut.get() would block indefinitely
Caso 3: troppi futuri
int test()
{
std::promise<int> pr;
auto fut1 = pr.get_future();
auto fut2 = pr.get_future(); // Error: "Future already retrieved"
return 0;
}
Caso 4: promessa soddisfatta
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".
Caso 5: troppa soddisfazione
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();
}
La stessa eccezione viene generata se c'è più di uno di uno di set_value
o set_exception
.
Caso 6: eccezione
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
Caso 7: promessa non mantenuta
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
} // Error: "broken promise"
return fut.get();
}