Programación concurrente (IV)

 Artículos de la serie:


¿Cómo funciona std::packaged_task?


En este artículo centraremos nuestra atención en el modo en que se encuentra implementada la plantilla de clase std::packaged_task<> [1], la cual, como analizamos en el post anterior de la serie, constituye un envoltorio para cualquier objeto invocable (funciones, expresiones lambda, etcétera) que deseemos ejecutar asíncronamente. La función miembro pública get_future() de std::packaged_task<> proporciona un objeto std::future<> como método de acceso al estado compartido almacenado en su interior.

El diseño básico que presentaremos para esta plantilla, simplificado con el fin de facilitar nuestro estudio y que renombraremos como Packaged_task<> (nótese la 'P' mayúscula), descansará en tres ingredientes fundamentales:
  • std::function<>, un envoltorio polimórfico para funciones disponible en el fichero de cabecera <functional> que puede almacenar y ejecutar cualquier objeto invocable (en nuestro caso, la operación a realizar asíncronamente) [2]. El parámetro de la plantilla debe coincidir con la signatura de dicha operación. Se trata del primer dato miembro de nuestra implementación de Packaged_task<>. Como ejemplo del modo de funcionamiento de std::function<>, el siguiente código imprime "Hello, world!":
      using namespace std;      auto print = function<void(string)>{[](string const& s){ cout << s; }};      print("Hello, world!");
    • std::future<>, que como sabemos proporciona una clase proxy para el resultado de la operación asíncrona [3]. El objeto de esta clase se obtendrá invocando a la función miembro get_future() de Packaged_task<>. Al igual que en el caso de la clase estándar, dicha función sólo puede llamarse una vez para cada objeto Packaged_task.
    • std::promise<>, el mecanismo por el cual podremos almacenar el resultado de la operación (sea éste un valor o una excepción) y transmitirlo asíncronamente al agente solicitante mediante el objeto std::future<> señalado anteriormente [4]. El objeto std::promise constituye el segundo dato miembro de Packaged_task<>. Como tal, éste se ve típicamente trasladado junto al resto del objeto Packaged_task a un nuevo hilo de ejecución a través de una operación std::move() (véase el post anterior de la serie). El objeto std::future<> asociado permanecerá, por contra, en el hilo original (caller thread), de forma que la pareja (promise,future) constituye el canal de comunicación entre los dos hilos en juego (véase la figura inferior), siendo promise su extremo push.


    De acuerdo con lo expuesto, nuestra versión simplificada de std::packaged_task<> respondería al esquema básico siguiente:

    #include <exception>  // incluye std::current_exception() #include <functional> // incluye std::function<> #include <future>     // incluye std::future<> y std::promise<> #include <utility>    // incluye std::move template<typename Res, typename... Args> // plantilla primaria (sin especificar) class Packaged_task; template<typename Res, typename... Args> // especialización parcial class Packaged_task<Res(Args...)> {      std::function<Res(Args...)> func_; // (1)     std::promise<Res> promise_;        // (2) public:     explicit Packaged_task(std::function<Res(Args...)> func)   // (A)        : func_{std::move(func)} { }     auto get_future() -> std::future<Res> { return promise_.get_future(); } // (B)     void operator()(Args&&... args) // (C)     try {          promise_.set_value(func_(std::forward<Args>(args)...));  // (D)       }       catch (...) {          promise_.set_exception(std::current_exception());  // (E)       } };

    El constructor de la clase (A) recibe un objeto std::function que envuelve cualquier objeto invocable de signatura Res(Args...) y lo almacena como primer dato miembro (1). El segundo dato miembro (2) es un objeto de tipo std::promise que almacenará el valor o excepción generado por la ejecución de la función. Este resultado podrá ser comunicado asíncronamente a un objeto std::future asociado, que podemos obtener mediante una llamada a la función miembro get_future() de la clase std::promise (B).

    El operador llamada a función operator() ejecutará finalmente la función (C) con los argumentos proporcionados por el usuario. El objeto std::promise tratará entonces de almacenar el valor de forma atómica mediante su función miembro set_value() para hacerlo disponible a través del objeto std::future asociado (D). De producirse una excepción, será ésta la comunicada a std::future mediante una llamada a set_exception() (E). Tanto set_value() como set_exception() marcan el estado compartido como listo para su uso (ready) y desbloquean el hilo que esté aguardando el resultado a través de un objeto std::future asociado.

    Observemos finalmente que al ser std::promise movible pero no copiable, Packaged_task adquiere el mismo comportamiento.


    Referencias bibliográficas
    1. cppreference - std::packaged_task - https://en.cppreference.com/w/cpp/thread/packaged_task
    2. cppreference - std::function - https://en.cppreference.com/w/cpp/utility/functional/function
    3. cppreference - std::future - https://en.cppreference.com/w/cpp/thread/future
    4. cppreference - std::promise - https://en.cppreference.com/w/cpp/thread/promise

    No hay comentarios:

    Publicar un comentario