c++ ¿Qué es std :: promesa?





4 Answers

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();
}
c++ multithreading c++11 promise standard-library

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?




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




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.




Related