¿Qué es std :: promise?

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 entender qué es la std::promise , qué hace y en qué situaciones se usa 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 solo :: hilo .

¿Podría alguien dar un breve y sucinto ejemplo de una situación en la que se necesita una std::promise estándar y donde es la solución más idiomática?

En las palabras de [futures.state], std::future es un objeto de retorno asincrónico (“un objeto que lee resultados de un estado compartido”) y std::promise es un proveedor asincrónico (“un objeto que proporciona un resultado a un estado compartido “), es decir, una promesa es aquello en que usted establece un resultado, de modo que puede obtenerlo del futuro asociado.

El proveedor asincrónico es lo que inicialmente crea el estado compartido al que se refiere el futuro. std::promise es un tipo de proveedor asincrónico, 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 comparta ese estado, y puede preparar el estado.

std::async es una utilidad de conveniencia de alto nivel que le proporciona un objeto de resultado asincrónico y se encarga internamente de crear el proveedor asíncrono y de preparar el estado compartido cuando la tarea finaliza. Podrías emularlo con std::packaged_task (o std::bind y std::promise ) y un std::thread pero es más seguro y fácil de usar std::async .

std::promise es un nivel un poco más bajo, para cuando quiere pasar un resultado asíncrono al futuro, pero el código que prepara el resultado no puede incluirse en una sola función adecuada para pasar a std::async . Por ejemplo, puede tener una matriz de varias promise y future asociados y tener un solo hilo que hace varios cálculos y establece un resultado en cada promesa. async solo le permitiría devolver un solo resultado, para devolver varios necesitaría llamar async varias veces, lo que podría desperdiciar recursos.

Entiendo la situación un poco mejor ahora (¡no por una pequeña cantidad debido a las respuestas aquí!), Así que pensé en agregar un pequeño artículo mío.


Hay dos conceptos distintos, aunque relacionados, en C ++ 11: cálculo asincrónico (una función que se llama en otro lugar) y ejecución simultánea (un hilo , algo que sí funciona al mismo tiempo). Los dos son conceptos algo ortogonales. El cálculo asincrónico es solo un sabor diferente de 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.

Hay una jerarquía de abstracción para el cálculo asincrónico. Por ejemplo, supongamos que tenemos una función que toma algunos argumentos:

 int foo(double, char, bool); 

En primer lugar, tenemos la plantilla std::future , que representa un valor futuro de tipo T El valor se puede recuperar a través de la función miembro get() , que efectivamente sincroniza el progtwig esperando el resultado. Alternativamente, un futuro admite wait_for() , que se puede usar para probar si el resultado ya está disponible o no. Se debe pensar en los futuros como el reemplazo sincrónico de reemplazo para los tipos de retorno ordinarios. Para nuestra función de ejemplo, esperamos un std::future .

Ahora, a la jerarquía, del nivel más alto al más bajo:

  1. std::async : la forma más conveniente y sencilla de realizar un cálculo asincrónico es mediante la plantilla de función async , que devuelve inmediatamente el futuro coincidente:

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

    Tenemos muy poco control sobre los detalles. En particular, ni siquiera sabemos si la función se ejecuta concurrentemente, en serie en get() , o por alguna otra magia negra. Sin embargo, el resultado se obtiene fácilmente cuando es necesario:

     auto res = fut.get(); // is an int 
  2. Ahora podemos considerar cómo implementar algo así 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 hilo separado por medio de la clase std::thread .

    El próximo 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í es invocable, y llamarlo es a discreción del usuario. Podemos configurarlo de esta manera:

     std::packaged_task tsk(foo); auto fut = tsk.get_future(); // is a std::future 

    El futuro está listo una vez que llamamos a la tarea y la llamada finaliza. 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 , o join al final del scope, o en cualquier momento (por ejemplo, usando el contenedor scoped_thread Anthony Williams, que realmente debería estar en la biblioteca estándar). Los detalles de usar std::thread no nos conciernen aquí, sin embargo; solo asegúrate de unirte o desvincularse eventualmente. 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 
  3. Ahora estamos en el nivel más bajo: ¿cómo implementaríamos la tarea empaquetada? Aquí es donde entra en std::promise estándar. La promesa es la piedra angular para comunicarse con un futuro. Los principales pasos son estos:

    • El hilo de llamada hace una promesa.

    • El hilo de llamada 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.

    Como ejemplo, esta es nuestra propia “tarea empaquetada”:

     template  class my_task; template  class my_task { std::function fn; std::promise pr; // the promise of the result public: template  explicit my_task(Ts &&... ts) : fn(std::forward(ts)...) { } template  void operator()(Ts &&... ts) { pr.set_value(fn(std::forward(ts)...)); // fulfill the promise } std::future 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 incluye mover la promesa. En situaciones más ad-hoc, también se podría mover un objeto promesa explícitamente al nuevo hilo y convertirlo en un argumento funcional de la función 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 por completo, por lo que se lanzan excepciones cada vez 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 . Primero, 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 ser satisfecha a través de set_value() o tiene una excepción establecida a través de set_exception() antes de que termine su vida útil si su futuro se va a consumir. Una promesa satisfecha puede morir sin consecuencias, y get() estará disponible en el futuro. Una promesa con una excepción elevará la excepción almacenada al momento de llamar a get() en el futuro. Si la promesa no tiene valor ni excepción, llamar a get() en el futuro generará una excepción de “promesa fallida”.

Aquí hay una pequeña serie de pruebas para demostrar estos diversos comportamientos excepcionales. Primero, el arnés:

 #include  #include  #include  #include  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 pr; return 0; } // fine, no problems 

Caso 2: promesa activa, sin usar

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

Caso 3: Demasiados futuros

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

Caso 4: promesa cumplida

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_value(10); } return fut.get(); } // Fine, returns "10". 

Caso 5: Demasiada satisfacción

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); pr2.set_value(10); pr2.set_value(10); // Error: "Promise already satisfied" } return fut.get(); } 

Se lanza la misma excepción si hay más de uno de set_value o set_exception .

Caso 6: Excepción

 int test() { std::promise pr; auto fut = pr.get_future(); { std::promise 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 pr; auto fut = pr.get_future(); { std::promise pr2(std::move(pr)); } // Error: "broken promise" return fut.get(); } 

Bartosz Milewski proporciona una buena reseña.

C ++ divide la implementación de futuros en un conjunto de pequeños bloques

std :: promise es una de estas partes.

Una promesa es un vehículo para pasar el valor de retorno (o una excepción) del hilo que ejecuta una función al hilo que aprovecha la función futura.

Un futuro es el objeto de sincronización construido alrededor del extremo receptor del canal de promesas.

Entonces, si desea usar un futuro, termina con una promesa que utiliza para obtener el resultado del procesamiento asincrónico.

Un ejemplo de la página es:

 promise intPromise; future intFuture = intPromise.get_future(); std::thread t(asyncFun, std::move(intPromise)); // do some other stuff int result = intFuture.get(); // may throw MyException 

En una aproximación aproximada, puedes considerar std::promise como el otro extremo de un std::future (esto es falso , pero para ilustrar puedes pensar como si fuera). El extremo consumidor del canal de comunicación usaría std::future para consumir el dato del estado compartido, mientras que el hilo productor usaría una std::promise para escribir en el estado compartido.

std::promise es el canal o camino para que se devuelva información 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 de retorno transportado en std::promise esté listo (es decir, su valor se establece dentro de la función).

En realidad, hay 3 entidades principales en el procesamiento asincrónico. C ++ 11 actualmente se centra en 2 de ellos.

Las cosas centrales que necesita para ejecutar un poco de lógica de forma asincrónica son:

  1. La tarea (lógica empaquetada como un objeto functor) que CORRERÁ ‘en algún lugar’.
  2. El nodo de procesamiento real : un hilo, un proceso, etc. que ejecuta dichos funtores cuando se le proporcionan. Mire el patrón de diseño “Comando” para tener una buena idea de cómo lo hace un grupo básico de subprocesos de trabajo.
  3. El mango del resultado : alguien necesita ese resultado y necesita un objeto que lo OBTENDRÁ. Para OOP y otras razones, cualquier espera o sincronización debe hacerse en las API de este manejador.

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 progtwigs reales necesitan administrar recursos de hilos y memoria, y la mayoría querrá que las tareas se ejecuten en grupos de hilos en lugar de crear y destruir un hilo para cada pequeña tarea (que casi siempre causa aciertos de rendimiento innecesarios y puede crear recursos fácilmente). inanición que es aún peor).

De acuerdo con Herb Sutter y otros en el C ++ 11 brain trust, hay planes tentativos para agregar un std::executor , al igual que en Java, será la base para 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 ayude si fracasan con el estándar para estos).

Una std::promise se crea como un punto final para un par promesa / futuro y std::future (creado a partir de std :: promesa utilizando el método get_future() ) es el otro punto final. Este es un método simple y único para proporcionar una manera de que dos hilos se sincronicen, ya que un hilo proporciona datos a otro hilo a través de un mensaje.

Usted puede pensarlo ya que un hilo crea una promesa de proporcionar datos y el otro hilo recoge la promesa en el futuro. Este mecanismo solo puede usarse una vez.

El mecanismo de promesa / futuro es solo una dirección, desde el hilo que usa el método set_value() de std::promise al hilo que usa get() de 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 hilo con std::promise no ha utilizado set_value() para cumplir su promesa, cuando el segundo hilo llame a get() de std::future para recoger la promesa, el segundo hilo entrará en estado de espera hasta que el la promesa se cumple con el primer hilo con std::promise cuando usa el método set_value() para enviar los datos.

El siguiente código de ejemplo, una aplicación de consola de Windows Visual Studio 2013 simple, muestra el uso de algunas de las clases / plantillas de concurrencia de C ++ 11 y otras funcionalidades. Ilustra un uso para promesa / futuro que funciona bien, subprocesos autónomos que realizarán una cierta tarea 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 es los retrasos agregados en varios lugares. Estas demoras se agregaron solo para asegurarse de que los diversos mensajes impresos en la consola mediante std::cout fueran claros y que el texto de los diversos hilos no se entremezclase.

La primera parte de main() crea tres subprocesos adicionales y usa std::promise y std::future para enviar datos entre los subprocesos. Un punto interesante es donde el hilo principal inicia un hilo, T2, que esperará datos del hilo principal, hará algo y luego enviará datos al tercer hilo, T3, que luego hará algo y enviará datos al Hilo principal.

La segunda parte de main() crea dos hilos y un conjunto de colas para permitir múltiples mensajes del hilo principal a cada uno de los dos hilos creados. No podemos usar std::promise y std::future para esto porque el dúo de promesa / futuro son de una sola vez y no se pueden usar repetidamente.

La fuente de la clase Sync_queue proviene del lenguaje de progtwigción C ++ de Stroustrup: 4ª edición.

 // cpp_threads.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include  #include  // std::thread is defined here #include  // std::future and std::promise defined here #include  // std::list which we use to build a message queue on. static std::atomic 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 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  q; // list that is used as a message queue }; template void Sync_queue::put(const Ttype &val) { std::lock_guard  lck(mtx); q.push_back(val); cond.notify_one(); } template void Sync_queue::get(Ttype &val) { std::unique_lock 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 &jj) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future 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 &jj, std::promise&pp) { int myId = std::atomic_fetch_add(&kount, 1); // get my identifier std::future 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 &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 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 intPromiseT2; std::promise 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 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 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 q1; // notification queue for messages to thread t11 Sync_queue 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 el siguiente resultado.

 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 

La promesa es el otro extremo del cable.

Imagine que necesita recuperar el valor de un future calculado por una async . Sin embargo, no desea que se compute en el mismo hilo, y ni siquiera genera un hilo “ahora”: tal vez su software fue diseñado para elegir un hilo de una agrupación, por lo que no sabe quién lo hará. realizar che computación al final.

Ahora, ¿qué pasas a este (aún desconocido) thread / class / entity? No pasas el future , ya que este es el resultado . Desea pasar algo que está conectado al future y que representa el otro extremo del cable , por lo que solo consultará el future sin conocimiento sobre quién realmente calculará / escribirá algo.

Esta es la promise . Es un mango conectado a su future . Si el future es un altavoz , y con get() empiezas a escuchar hasta que sale un 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, solo démelo y espere hasta que la otra parte diga algo.