c++ multithreading c++11 promise standard-library

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:

  1. 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ón async , 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

  2. 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 clase std::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 , o join al final del alcance, o cuando sea (por ejemplo, usando el envoltorio scoped_thread Anthony Williams, que realmente debería estar en la biblioteca estándar). Sin embargo, los detalles del uso de std::thread no nos conciernen aquí; Solo asegúrate de unirte o thr 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

  3. 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 de set_exception() antes de que finalice su vida útil si se va a consumir su futuro. Una promesa satisfecha puede morir sin consecuencias, y get() estará disponible en el futuro. Una promesa con una excepción aumentará la excepción almacenada en la llamada de get() en el futuro. Si la promesa muere sin valor ni excepción, llamar a get() 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:

  1. La tarea (lógica empaquetada como un objeto de funtor) que SE EJECUTARÁ ''en algún lugar''.
  2. 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.
  3. 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.