Estoy bastante familiarizado con C ++ 11 del std::thread
, std::async
y std::future
los componentes (por ejemplo, véase esta respuesta ), que son sencillas.
Sin embargo, no puedo entender qué std::promise
es, qué hace y en qué situaciones se usa mejor. El documento estándar en sí no contiene mucha información más allá de su sinopsis de clase, y tampoco lo hace solo :: thread .
¿Podría alguien dar un breve y sucinto ejemplo de una situación en la que std::promise
se necesita y donde es la solución más idiomática?
c++
multithreading
c++11
promise
standard-library
Kerrek SB
fuente
fuente
std::promise
es de dóndestd::future
viene.std::future
es lo que le permite recuperar un valor que se le ha prometido . Cuando llamaget()
a un futuro, espera hasta que el propietario delstd::promise
con el que establece el valor (llamandoset_value
a la promesa). Si la promesa se destruye antes de establecer un valor, y luego invocasget()
un futuro asociado con esa promesa, obtendrás unastd::broken_promise
excepción porque se te prometió un valor, pero es imposible que lo consigas.std::broken_promise
es el identificador mejor nombrado en la biblioteca estándar. Y allí no estástd::atomic_future
.Respuestas:
En palabras de [futures.state] a
std::future
es un objeto de retorno asíncrono ("un objeto que lee resultados de un estado compartido") y astd::promise
es un proveedor asíncrono ("un objeto que proporciona un resultado a un estado compartido"), es decir, un Promesa es aquello en lo que establece un resultado, para que pueda obtenerlo del futuro asociado.El proveedor asincrónico es lo que inicialmente crea el estado compartido al que se refiere un futuro.
std::promise
es un tipo de proveedor asincrónico,std::packaged_task
es otro, y el detalle interno destd::async
es otro. Cada uno de ellos puede crear un estado compartido y darle un estadostd::future
que lo comparta, y puede preparar el estado.std::async
es una utilidad de conveniencia de nivel superior que le brinda un objeto de resultado asíncrono y se encarga internamente de crear el proveedor asíncrono y de preparar el estado compartido cuando se completa la tarea. Puede emularlo con unstd::packaged_task
(ostd::bind
y astd::promise
) y unstd::thread
pero es más seguro y fácil de usarstd::async
.std::promise
es un poco más bajo, para cuando desea pasar un resultado asincrónico al futuro, pero el código que hace que el resultado esté listo no se puede incluir en una sola función adecuada para pasarstd::async
. Por ejemplo, puede tener una matriz de variospromise
sy asociadosfuture
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 llamarasync
varias veces, lo que podría desperdiciar recursos.fuente
future
es un ejemplo concreto de un objeto de retorno asíncrono , que es un objeto que lee un resultado que se devolvió de forma asíncrona, a través del estado compartido. Apromise
es un ejemplo concreto de un proveedor asincrónico , que es un objeto que escribe un valor en el estado compartido, que se puede leer de forma asincrónica. Quise decir lo que escribí.Ahora entiendo la situación un poco mejor (¡en gran medida debido a las respuestas aquí!), Así que pensé agregar un poco de mi propia reseña.
Hay dos conceptos distintos, aunque relacionados, en C ++ 11: computación asincrónica (una función que se llama en otro lugar) y ejecución concurrente (un hilo , algo que funciona simultáneamente). Los dos son conceptos algo ortogonales. El cálculo asincrónico es solo un sabor diferente de la llamada de función, mientras que un hilo 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 asincrónico. Por ejemplo, supongamos que tenemos una función que toma algunos argumentos:
En primer lugar, tenemos la plantilla
std::future<T>
, que representa un valor futuro de tipoT
. El valor se puede recuperar a través de la función miembroget()
, que sincroniza efectivamente el programa esperando el resultado. Alternativamente, un futuro admitewait_for()
, que se puede utilizar para investigar si el resultado ya está disponible o no. Los futuros deben considerarse como el reemplazo asíncrono directo para los tipos de retorno ordinarios. Para nuestra función de ejemplo, esperamos astd::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 asincrónico es a través de laasync
plantilla de función, que devuelve el futuro coincidente de inmediato:Tenemos muy poco control sobre los detalles. En particular, ni siquiera sabemos si la función se ejecuta simultáneamente, en serie
get()
o por alguna otra magia negra. Sin embargo, el resultado se obtiene fácilmente cuando es necesario:Ahora podemos considerar la forma de poner en práctica algo así
async
, pero de una manera que nos controlar. 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 lastd::thread
clase.El siguiente nivel de abstracción más bajo 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 queda a discreción del usuario. Podemos configurarlo así:El futuro se prepara 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:
El hilo comienza a ejecutarse inmediatamente. Podemos
detach
hacerlo, o tenerlojoin
al final del alcance, o cuando sea (por ejemplo, usando elscoped_thread
contenedor de Anthony Williams , que realmente debería estar en la biblioteca estándar). Sinstd::thread
embargo, los detalles del uso no nos conciernen aquí; solo asegúrese de unirse o separarsethr
eventualmente. Lo importante es que cada vez que finalice la llamada a la función, nuestro resultado esté listo:Ahora estamos en el nivel más bajo: ¿cómo implementaríamos la tarea empaquetada? Aquí es donde
std::promise
entra en juego. La promesa es la piedra angular para comunicarse con un futuro. Los pasos principales son estos:El hilo conductor hace una promesa.
El hilo conductor llama a 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 cumple la promesa.
El hilo original recupera el resultado.
Como ejemplo, aquí está nuestra propia "tarea empaquetada":
El uso de esta plantilla es esencialmente el mismo que el de
std::packaged_task
. Tenga en cuenta que mover toda la tarea supone mover la promesa. En situaciones más ad-hoc, uno también podría mover un objeto de promesa explícitamente al nuevo hilo y convertirlo en un argumento de función de la función del 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 cuando una operación de una promesa no tiene sentido. Todas las excepciones son de tipo
std::future_error
, que deriva destd::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
set_value()
o tener una excepción establecidaset_exception()
antes de que termine 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 cuando se soliciteget()
en el futuro. Si la promesa muere sin valor ni excepción, invocarget()
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:
Ahora a las pruebas.
Caso 1: promesa inactiva
Caso 2: promesa activa, no utilizada
Caso 3: demasiados futuros
Caso 4: promesa satisfecha
Caso 5: demasiada satisfacción
Se produce la misma excepción si hay más de uno de cualquiera de
set_value
oset_exception
.Caso 6: Excepción
Caso 7: promesa rota
fuente
std::function
tiene muchos constructores; no hay razón para no exponerlos al consumidor demy_task
.get()
al futuro genera una excepción. Aclararé esto agregando "antes de que sea destruido"; por favor avíseme si eso está suficientemente claro.got()
hefuture
encontrado la biblioteca de soporte de hilos enpromise
tu increíble explicación!Bartosz Milewski ofrece una buena crítica.
std :: promise es una de estas partes.
...
Por lo tanto, si desea utilizar un futuro, termina con una promesa que utiliza para obtener el resultado del procesamiento asincrónico.
Un ejemplo de la página es:
fuente
En una aproximación aproximada, puede considerarlo
std::promise
como el otro extremo de astd::future
(esto es falso , pero por ejemplo, puede pensar como si lo fuera). El consumidor final del canal de comunicación usaría astd::future
para consumir el dato desde el estado compartido, mientras que el hilo productor usaría astd::promise
para escribir en el estado compartido.fuente
std::async
puede conceptualmente (esto no es obligatorio por el estándar) entendido como una función que crea unstd::promise
, lo empuja a un grupo de subprocesos (más o menos, podría ser un grupo de subprocesos, podría ser un nuevo subproceso, ...) y devuelve el asociadostd::future
a la persona que llama. En el lado del cliente, esperaría en elstd::future
y un hilo en el otro extremo calcularía el resultado y lo almacenaría en elstd::promise
. Nota: el estándar requiere el estado compartido ystd::future
la existencia de astd::promise
en este caso de uso en particular, pero no .std::future
no llamarájoin
al hilo, tiene un puntero a un estado compartido que es el búfer de comunicación real. El estado compartido tiene un mecanismo de sincronización (probablementestd::function
+std::condition_variable
para bloquear a la persona que llama hasta questd::promise
se complete. La ejecución del subproceso es ortogonal a todo esto, y en muchas implementaciones puede encontrar questd::async
no se ejecutan nuevos subprocesos que luego se unen, pero más bien por un grupo de hilos cuya vida útil se extiende hasta el final del programa.std::async
es que la biblioteca de tiempo de ejecución puede tomar las decisiones correctas para usted con respecto a la cantidad de subprocesos a crear y, en la mayoría de los casos, esperaré tiempos de ejecución que usen grupos de subprocesos. Actualmente VS2012 usa un grupo de subprocesos debajo del capó, y no viola la regla de si-como . Tenga en cuenta que hay muy pocas garantías que deben cumplirse para este particular como si .std::promise
es el canal o la vía para que la información se devuelva 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 transportadostd::promise
esté listo (lo que significa que su valor se establece dentro de la función).fuente
Realmente hay 3 entidades centrales en el procesamiento asincrónico. C ++ 11 actualmente se centra en 2 de ellos.
Las cosas principales que necesita para ejecutar algo de lógica de forma asincrónica son:
C ++ 11 llama a las cosas de las que hablo en (1)
std::promise
y a las de (3)std::future
.std::thread
es lo único que se proporciona públicamente para (2). Esto es desafortunado porque los programas reales necesitan administrar 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 (que casi siempre causa problemas de rendimiento innecesarios por sí mismo y puede crear recursos fácilmente inanición que es aún peor).De acuerdo con Herb Sutter y otros en la confianza del cerebro de C ++ 11, existen planes tentativos para agregar que
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 como C ++ 17 (y que Dios nos ayude si no cumplen con el estándar para estos).fuente
A
std::promise
se crea como un punto final para un par promesa / futuro y elstd::future
(creado a partir de std :: promise usando elget_future()
método) es el otro punto final. Este es un método simple, de una sola toma, 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.Puede pensar que un hilo crea una promesa de proporcionar datos y el otro hilo recoge la promesa en el futuro. Este mecanismo solo se puede usar una vez.
El mecanismo de promesa / futuro es solo una dirección, desde el hilo que usa el
set_value()
método de astd::promise
hasta el hilo que usa elget()
de astd::future
para recibir los datos. Se genera una excepción si elget()
método de un futuro se llama más de una vez.Si el subproceso con el
std::promise
no se ha utilizadoset_value()
para cumplir su promesa, cuando el segundo subproceso llameget()
alstd::future
para recopilar la promesa, el segundo subproceso pasará a un estado de espera hasta que el primer subproceso con la promesa cumpla la promesastd::promise
cuando utilice elset_value()
método para enviar los datos.Con las corutinas propuestas de los lenguajes de programación de la especificación técnica N4663 - Extensiones C ++ para corutinas y el soporte del compilador C ++ de Visual Studio 2017
co_await
, también es posible usarstd::future
ystd::async
escribir la funcionalidad corutina. Consulte la discusión y el ejemplo en https://stackoverflow.com/a/50753040/1466970 que tiene como una sección que discute el uso destd::future
withco_await
.El siguiente código de ejemplo, una simple aplicación de consola de Windows de Visual Studio 2013, muestra el uso de algunas de las clases / plantillas de concurrencia de C ++ 11 y otras funciones. Ilustra un uso para promesa / futuro que funciona bien, hilos autónomos que harán alguna 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 son las demoras agregadas en varios lugares. Estos retrasos se agregaron solo para asegurarse de que los diversos mensajes impresos en la consola que se usan
std::cout
sean claros y que el texto de varios hilos no se mezcle.La primera parte de esto
main()
es crear tres hilos adicionales y usarstd::promise
ystd::future
enviar datos entre los hilos. Un punto interesante es donde el subproceso principal inicia un subproceso, T2, que esperará los datos del subproceso principal, hará algo y luego enviará datos al tercer subproceso, T3, que luego hará algo y enviará datos de regreso al Hilo principal.La segunda parte de
main()
crea dos hilos y un conjunto de colas para permitir múltiples mensajes desde el hilo principal a cada uno de los dos hilos creados. No podemos usarlostd::promise
ystd::future
para esto porque el dúo prometedor / futuro es de una sola vez y no se puede usar repetidamente.La fuente de la clase
Sync_queue
es del lenguaje de programación C ++ de Stroustrup: 4ta edición.Esta sencilla aplicación crea el siguiente resultado.
fuente
La promesa es el otro extremo del cable.
Imagine que necesita recuperar el valor de un
future
ser calculado por unasync
. Sin embargo, no desea que se calcule en el mismo hilo, y ni siquiera genera un hilo "ahora"; tal vez su software fue diseñado para elegir un hilo de un grupo, por lo que no sabe quién lo hará realizar che computación al final.Ahora, ¿qué le pasa a este (todavía desconocido) hilo / clase / entidad? No pasa el
future
, ya que este es el resultado . Desea pasar algo que está conectado alfuture
y que representa el otro extremo del cable , por lo que simplemente consultaráfuture
sin saber quién realmente calculará / escribirá algo.Este es el
promise
. Es un mango conectado a tufuture
. Sifuture
es un altavoz yget()
comienza a escuchar hasta que sale algo de sonido,promise
es un micrófono ; pero no cualquier micrófono, es el micrófono conectado con un solo cable al altavoz que tiene. Es posible que sepa quién está en el otro extremo, pero no es necesario que lo sepa, solo délo y espere hasta que la otra parte diga algo.fuente
http://www.cplusplus.com/reference/future/promise/
Explicación de una oración: furture :: get () espera promse :: set_value () para siempre.
fuente