[c++] ¿Qué es std :: promise?



Answers

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 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 efectivamente sincroniza el programa 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<int> .

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<int>
    

    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<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 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 alcance, 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 <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 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 <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 usar

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 cumplida

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();
}

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<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();
}
Question

Estoy bastante familiarizado con los nuevos componentes estándar std::thread , std::async y std::future la nueva biblioteca estándar (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 just::thread .

¿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 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.




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.




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 programas 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).




Related