c++ - ¿Qué es std:: promesa?
multithreading c++11 (8)
Estoy bastante familiarizado con std::future
componentes std::thread
, std::async
y std::future
C ++ 11 (por ejemplo, vea esta respuesta ), que son sencillos.
Sin embargo, no puedo comprender qué es std::promise
, qué hace y en qué situaciones se utiliza mejor. El documento estándar en sí no contiene una gran cantidad de información más allá de su sinopsis de clase, y tampoco lo hace just::thread .
¿Podría alguien dar un breve y breve ejemplo de una situación en la que se necesita una std::promise
y la solución más idiomática?
Ahora comprendo un poco mejor la situación (¡en gran parte debido a las respuestas aquí!), Así que pensé que agregaría un poco de mi propia reseña.
Hay dos conceptos distintos, aunque relacionados, en C ++ 11: cómputo asíncrono (una función que se llama en otro lugar) y ejecución concurrente (un subproceso , algo que funciona simultáneamente). Los dos son conceptos algo ortogonales. La computación asíncrona es solo un sabor diferente de la llamada de función, mientras que un subproceso es un contexto de ejecución. Los hilos son útiles por derecho propio, pero para el propósito de esta discusión, los trataré como un detalle de implementación.
Existe una jerarquía de abstracción para el cálculo asíncrono. Por ejemplo, supongamos que tenemos una función que toma algunos argumentos:
int foo(double, char, bool);
En primer lugar, tenemos la plantilla en.cppreference.com/w/cpp/thread/future , que representa un valor futuro de tipo T
El valor se puede recuperar a través de la función miembro get()
, que sincroniza efectivamente el programa esperando el resultado. Alternativamente, un futuro es compatible con wait_for()
, que se puede usar para probar si el resultado ya está disponible. Los futuros deben considerarse como el reemplazo directo asíncrono para los tipos de retorno ordinarios. Para nuestra función de ejemplo, esperamos un std::future<int>
.
Ahora, en la jerarquía, del nivel más alto al más bajo:
std::async
: la forma más conveniente y directa de realizar un cálculo asíncrono es a través de la plantilla de funciónasync
, que devuelve el futuro coincidente de inmediato:auto fut = std::async(foo, 1.5, ''x'', false); // is a std::future<int>
Tenemos muy poco control sobre los detalles. En particular, ni siquiera sabemos si la función se ejecuta al mismo tiempo, en serie en
get()
, o por algún otro tipo de magia negra. Sin embargo, el resultado se obtiene fácilmente cuando es necesario:auto res = fut.get(); // is an int
Ahora podemos considerar cómo implementar algo como
async
, pero de una manera que controlamos. Por ejemplo, podemos insistir en que la función se ejecute en un hilo separado. Ya sabemos que podemos proporcionar un subproceso separado por medio de la clasestd::thread
.El siguiente nivel inferior de abstracción hace exactamente eso:
std::packaged_task
. Esta es una plantilla que envuelve una función y proporciona un futuro para el valor de retorno de las funciones, pero el objeto en sí mismo es invocable, y llamarlo es a criterio del usuario. Podemos configurarlo de esta manera:std::packaged_task<int(double, char, bool)> tsk(foo); auto fut = tsk.get_future(); // is a std::future<int>
El futuro está listo una vez que llamamos a la tarea y la llamada se completa. Este es el trabajo ideal para un hilo separado. Solo tenemos que asegurarnos de mover la tarea al hilo:
std::thread thr(std::move(tsk), 1.5, ''x'', false);
El hilo comienza a ejecutarse inmediatamente. Podemos
detach
, ojoin
al final del alcance, o cuando sea (por ejemplo, usando el envoltorioscoped_thread
Anthony Williams, que realmente debería estar en la biblioteca estándar). Sin embargo, los detalles del uso destd::thread
no nos conciernen aquí; Solo asegúrate de unirte othr
tiempo. Lo que importa es que cada vez que finaliza la llamada a la función, nuestro resultado está listo:auto res = fut.get(); // as before
Ahora estamos en el nivel más bajo: ¿Cómo implementaríamos la tarea empaquetada? Aquí es donde entra la
std::promise
. La promesa es la piedra angular para la comunicación con el futuro. Los pasos principales son estos:El hilo que llama hace una promesa.
El hilo que llama obtiene un futuro de la promesa.
La promesa, junto con los argumentos de la función, se mueven a un hilo separado.
El nuevo hilo ejecuta la función y llena cumple la promesa.
El hilo original recupera el resultado.
A modo de ejemplo, aquí está nuestra propia "tarea empaquetada":
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 };
El uso de esta plantilla es esencialmente el mismo que el de
std::packaged_task
. Tenga en cuenta que mover toda la tarea subsume mover la promesa. En situaciones más ad-hoc, también se puede mover un objeto de promesa explícitamente al nuevo hilo y convertirlo en un argumento de función de la función de hilo, pero un contenedor de tareas como el anterior parece una solución más flexible y menos intrusiva.
Haciendo excepciones
Las promesas están íntimamente relacionadas con las excepciones. La interfaz de una promesa por sí sola no es suficiente para transmitir su estado completamente, por lo que las excepciones se producen siempre que una operación en una promesa no tiene sentido. Todas las excepciones son de tipo std::future_error
, que deriva de std::logic_error
. En primer lugar, una descripción de algunas restricciones:
Una promesa construida por defecto está inactiva. Las promesas inactivas pueden morir sin consecuencias.
Una promesa se activa cuando se obtiene un futuro a través de
get_future()
. Sin embargo, solo se puede obtener un futuro!Una promesa debe cumplirse a través de
set_value()
o tener una excepción establecida a través deset_exception()
antes de que finalice su vida útil si se va a consumir su futuro. Una promesa satisfecha puede morir sin consecuencias, yget()
estará disponible en el futuro. Una promesa con una excepción aumentará la excepción almacenada en la llamada deget()
en el futuro. Si la promesa muere sin valor ni excepción, llamar aget()
en el futuro generará una excepción de "promesa rota".
Aquí hay una pequeña serie de pruebas para demostrar estos diversos comportamientos excepcionales. Primero, el arnés:
#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;
}
}
Ahora a las pruebas.
Caso 1: promesa inactiva
int test()
{
std::promise<int> pr;
return 0;
}
// fine, no problems
Caso 2: Promesa activa, sin uso.
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
return 0;
}
// fine, no problems; fut.get() would block indefinitely
Caso 3: Demasiados futuros
int test()
{
std::promise<int> pr;
auto fut1 = pr.get_future();
auto fut2 = pr.get_future(); // Error: "Future already retrieved"
return 0;
}
Caso 4: Promesa satisfecha
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: Demasiada satisfacción.
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 misma excepción se produce si hay más de uno de set_value
o set_exception
.
Caso 6: Excepción
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: promesa rota
int test()
{
std::promise<int> pr;
auto fut = pr.get_future();
{
std::promise<int> pr2(std::move(pr));
} // Error: "broken promise"
return fut.get();
}
En las palabras de [futures.state] un std::future
es un objeto de retorno asíncrono ("un objeto que lee resultados de un estado compartido") y un std::promise
es un proveedor asíncrono ("un objeto que proporciona un resultado a un estado compartido ") es decir, una promesa es lo que establece un resultado, para que pueda obtenerlo del futuro asociado.
El proveedor asíncrono es lo que inicialmente crea el estado compartido al que se refiere un futuro. std::promise
es un tipo de proveedor asíncrono, std::packaged_task
es otro, y el detalle interno de std::async
es otro. Cada uno de ellos puede crear un estado compartido y darle un std::future
que comparte ese estado y puede hacer que el estado esté listo.
std::async
es una utilidad de conveniencia de nivel superior que le proporciona un objeto de resultado asíncrono y se encarga internamente de crear el proveedor asíncrono y de preparar el estado compartido cuando finaliza la tarea. Podría emularlo con un std::packaged_task
(o std::bind
y un std::promise
) y un std::thread
pero es más seguro y más fácil de usar std::async
.
std::promise
es un poco más bajo, porque cuando se quiere pasar un resultado asíncrono al futuro, pero el código que hace que el resultado esté listo no se puede envolver en una sola función adecuada para pasar a std::async
. Por ejemplo, puede tener un conjunto de varias promise
y future
asociados y un solo hilo que realiza varios cálculos y establece un resultado en cada promesa. async
solo le permitiría devolver un solo resultado, para devolver varios, tendría que llamar async
varias veces, lo que podría desperdiciar recursos.
En una aproximación aproximada, puede considerar std::promise
como el otro extremo de un std::future
(esto es falso , pero para ilustrar puede pensar como si lo fuera). El extremo consumidor del canal de comunicación usaría un std::future
para consumir el dato del estado compartido, mientras que el subproceso productor usaría un std::promise
escribir en el estado compartido.
Realmente hay 3 entidades centrales en el procesamiento asíncrono. C ++ 11 actualmente se enfoca en 2 de ellos.
Las cosas principales que necesita para ejecutar un poco de lógica asincrónicamente son:
- La tarea (lógica empaquetada como un objeto de funtor) que SE EJECUTARÁ ''en algún lugar''.
- El nodo de procesamiento real : un subproceso, un proceso, etc., que FUNCIONA tales funtores cuando se le proporcionan. Mire el patrón de diseño "Comando" para tener una buena idea de cómo un grupo de subprocesos de trabajo básico hace esto.
- El controlador de resultados : Alguien necesita ese resultado y necesita un objeto que lo OBTENDRÁ por ellos. Por OOP y otras razones, cualquier espera o sincronización debe hacerse en las API de este identificador.
C ++ 11 llama a las cosas de las que hablo en (1) std::promise
, y aquellas en (3) std::future
. std::thread
es lo único que se proporciona públicamente para (2). Esto es desafortunado porque los programas reales necesitan administrar los recursos de subprocesos y memoria, y la mayoría querrá que las tareas se ejecuten en grupos de subprocesos en lugar de crear y destruir un subproceso para cada pequeña tarea (lo que casi siempre causa impactos de rendimiento innecesarios por sí mismo y puede crear recursos fácilmente). hambre que es aún peor).
Según Herb Sutter y otros en la confianza del cerebro de C ++ 11, hay planes tentativos para agregar un std::executor
estándar que, al igual que en Java, será la base para los grupos de subprocesos y configuraciones lógicamente similares para (2). Tal vez lo veamos en C ++ 2014, pero mi apuesta es más parecida a C ++ 17 (y Dios nos ayudará si rompen el estándar para estos).
Un std::promise
se crea como un punto final para una promesa / futuro par y std::future
(creado a partir de std :: promise utilizando el método get_future()
) es el otro punto final. Este es un método simple de un solo disparo para proporcionar una manera de sincronizar dos hilos, ya que un hilo proporciona datos a otro hilo a través de un mensaje.
Puede pensar que un hilo crea una promesa de proporcionar datos y el otro hilo recoge la promesa en el futuro. Este mecanismo solo puede ser usado una vez.
El mecanismo promesa / futuro es solo una dirección, desde el hilo que usa el método set_value()
de std::promise
al hilo que usa el get()
de un std::future
para recibir los datos. Se genera una excepción si el método get()
de un futuro se llama más de una vez.
Si el subproceso con std::promise
no ha usado set_value()
para cumplir su promesa, cuando el segundo subproceso llama a get()
de std::future
para recopilar la promesa, el segundo subproceso pasará a un estado de espera hasta que La promesa se cumple con el primer hilo con std::promise
cuando usa el método set_value()
para enviar los datos.
Con las correcciones propuestas de los lenguajes de programación de la Especificación técnica N4663 - Extensiones C ++ para Coroutines y el soporte del compilador Visual Studio 2017 C ++ de co_await
, también es posible usar std::future
y std::async
para escribir la funcionalidad de coroutine. Vea la discusión y el ejemplo en https://.com/a/50753040/1466970 que tiene como una sección que discute el uso de std::future
con co_await
.
El siguiente código de ejemplo, una sencilla aplicación de consola de Visual Studio 2013 para Windows, muestra cómo se usan algunas de las clases / plantillas de concurrencia de C ++ 11 y otras funciones. Ilustra un uso para promesa / futuro que funciona bien, subprocesos autónomos que realizarán algunas tareas y se detendrán, y un uso donde se requiere un comportamiento más sincrónico y debido a la necesidad de múltiples notificaciones, el par promesa / futuro no funciona.
Una nota sobre este ejemplo son los retrasos agregados en varios lugares. Estos retrasos se agregaron solo para asegurarse de que los diversos mensajes impresos en la consola usando std::cout
serían claros y que el texto de los varios subprocesos no se entremezclaría.
La primera parte de main()
es crear tres subprocesos adicionales y usar std::promise
y std::future
para enviar datos entre los subprocesos. Un punto interesante es que el hilo principal inicia un hilo, T2, que esperará los datos del hilo principal, hará algo y luego enviará datos al tercer hilo, T3, que luego hará algo y enviará datos a la Hilo principal.
La segunda parte de main()
crea dos subprocesos y un conjunto de colas para permitir múltiples mensajes desde el subproceso principal a cada uno de los dos subprocesos creados. No podemos usar std::promise
y std::future
para esto porque el dúo promesa / futuro es de un disparo y no se puede usar repetidamente.
La fuente de la clase Sync_queue
es de The C ++ Programming Language: 4th Edition de Stroustrup.
// 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;
}
Esta sencilla aplicación crea la siguiente salida.
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
Bartosz Milewski proporciona una buena reseña.
C ++ divide la implementación de futuros en un conjunto de pequeños bloques
std :: la promesa es una de estas partes.
Una promesa es un vehículo para pasar el valor de retorno (o una excepción) del subproceso que ejecuta una función al subproceso que se cobra en el futuro de la función.
...
Un futuro es el objeto de sincronización construido alrededor del extremo receptor del canal de promesa.
Entonces, si desea utilizar un futuro, termina con una promesa que utiliza para obtener el resultado del procesamiento asíncrono.
Un ejemplo de la página es:
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
std::promise
es el canal o vía para que la información sea devuelta desde la función asíncrona. std::future
es el mecanismo de sincronización que hace que la persona que llama espere hasta que el valor devuelto en std::promise
esté listo (lo que significa que su valor se establece dentro de la función).
La promesa es el otro extremo del cable.
Imagine que necesita recuperar el valor de un future
calculado por un async
. Sin embargo, no desea que se calcule en el mismo subproceso y ni siquiera genera un subproceso "ahora"; tal vez su software fue diseñado para seleccionar un subproceso de un grupo, por lo que no sabe quién lo hará. Realizar el cálculo de che al final.
Ahora, ¿qué pasas a este hilo / clase / entidad (aún desconocido)? No pasas el future
, ya que este es el resultado . Desea pasar algo que esté conectado al future
y que represente el otro extremo del cable , así que solo consultará el future
sin saber quién procesará / escribirá realmente algo.
Esta es la promise
. Es un mango conectado a tu future
. Si el future
es un altavoz , y con get()
empiezas a escuchar hasta que sale un poco de sonido, la promise
es un micrófono ; pero no solo cualquier micrófono, es el micrófono conectado con un solo cable al altavoz que sostiene. Es posible que sepa quién está en el otro extremo, pero no necesita saberlo, simplemente délo y espere hasta que la otra parte diga algo.