¿Cuál es la diferencia entre packaged_task y async?

134

Mientras trabajaba con el modelo roscado de C ++ 11, noté que

std::packaged_task<int(int,int)> task([](int a, int b) { return a + b; });
auto f = task.get_future();
task(2,3);
std::cout << f.get() << '\n';

y

auto f = std::async(std::launch::async, 
    [](int a, int b) { return a + b; }, 2, 3);
std::cout << f.get() << '\n';

parecen hacer exactamente lo mismo. Entiendo que podría haber una gran diferencia si corrierastd::async con std::launch::deferred, pero ¿hay alguna en este caso?

¿Cuál es la diferencia entre estos dos enfoques y, lo que es más importante, en qué casos de uso debo usar uno sobre el otro?

nijansen
fuente

Respuestas:

161

En realidad, el ejemplo que acaba de mostrar muestra las diferencias si utiliza una función bastante larga, como

//! sleeps for one second and returns 1
auto sleep = [](){
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 1;
};

Tarea empaquetada

A packaged_taskno se iniciará solo, debe invocarlo:

std::packaged_task<int()> task(sleep);

auto f = task.get_future();
task(); // invoke the function

// You have to wait until task returns. Since task calls sleep
// you will have to wait at least 1 second.
std::cout << "You can see this after 1 second\n";

// However, f.get() will be available, since task has already finished.
std::cout << f.get() << std::endl;

std::async

Por otro lado, std::asyncwith launch::asyncintentará ejecutar la tarea en un hilo diferente:

auto f = std::async(std::launch::async, sleep);
std::cout << "You can see this immediately!\n";

// However, the value of the future will be available after sleep has finished
// so f.get() can block up to 1 second.
std::cout << f.get() << "This will be shown after a second!\n";

Retirarse

Pero antes de intentar usarlo asyncpara todo, tenga en cuenta que el futuro devuelto tiene un estado compartido especial, que exige que future::~futurebloquee:

std::async(do_work1); // ~future blocks
std::async(do_work2); // ~future blocks

/* output: (assuming that do_work* log their progress)
    do_work1() started;
    do_work1() stopped;
    do_work2() started;
    do_work2() stopped;
*/

Entonces, si desea una verdadera asíncrona, debe conservar la devolución future, o si no le importa el resultado si las circunstancias cambian:

{
    auto pizza = std::async(get_pizza);
    /* ... */
    if(need_to_go)
        return;          // ~future will block
    else
       eat(pizza.get());
}   

Para obtener más información sobre esto, ver el artículo de Herb Sutter asyncy~future , que describe el problema, y Scott Meyer std::futuresde std::asyncque no son especiales , que describe los puntos de vista. También tenga en cuenta que este comportamiento se especificó en C ++ 14 y versiones posteriores , pero también se implementó comúnmente en C ++ 11.

Diferencias adicionales

Al usarlo std::async, ya no puede ejecutar su tarea en un hilo específico, donde std::packaged_taskse puede mover a otros hilos.

std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::thread myThread(std::move(task),2,3);

std::cout << f.get() << "\n";

Además, packaged_taskdebe invocarse antes de llamar f.get(), de lo contrario su programa se congelará ya que el futuro nunca estará listo:

std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::cout << f.get() << "\n"; // oops!
task(2,3);

TL; DR

Úselo std::asyncsi desea que se hagan algunas cosas y realmente no le importa cuándo se hacen, y std::packaged_tasksi desea concluir las cosas para moverlas a otros hilos o llamarlas más tarde. O, para citar a Christian :

Al final, a std::packaged_taskes solo una característica de nivel inferior para implementar std::async(por lo que puede hacer más que std::asyncsi se usa junto con otras cosas de nivel inferior, como std::thread). Simplemente dicho una std::packaged_taskes un std::functionvinculados a una std::futurey std::asyncenvolturas y llama a un std::packaged_task(posiblemente en un hilo diferente).

Zeta
fuente
9
Debe agregar que el futuro devuelto por bloques asíncronos en la destrucción (como si llamara get) mientras que el que regresó de packaged_task no.
John5342
22
Al final, a std::packaged_taskes solo una característica de nivel inferior para implementar std::async(por lo que puede hacer más que std::asyncsi se usa junto con otras cosas de nivel inferior, como std::thread). Simplemente dicho una std::packaged_taskes un std::functionvinculados a una std::futurey std::asyncenvolturas y llama a un std::packaged_task(posiblemente en un hilo diferente).
Christian Rau
Estoy haciendo algunos experimentos en el bloque ~ future (). No pude replicar el efecto de bloqueo en la futura destrucción de objetos. Todo funcionó asincrónicamente. Estoy usando VS 2013 y cuando inicio el asíncrono, usé std :: launch :: async. ¿VC ++ de alguna manera "solucionó" este problema?
Frank Liu
1
@FrankLiu: Bueno, N3451 es una propuesta aceptada, que (hasta donde yo sé) entró en C ++ 14. Dado que Herb trabaja en Microsoft, no me sorprendería si esa característica se implementa en VS2013. Un compilador que siga estrictamente las reglas de C ++ 11 aún mostraría este comportamiento.
Zeta
1
@Mikhail Esta respuesta precede tanto a C ++ 14 como a C ++ 17, por lo que no tenía los estándares, sino solo las propuestas disponibles. Eliminaré el párrafo.
Zeta
1

Tarea empaquetada vs asíncrono

p> La tarea empaquetada contiene una tarea[function or function object]y un par futuro / promesa. Cuando la tarea ejecuta una declaración de devolución, se produceset_value(..)lapackaged_taskpromesa de.

a> Dadas las tareas de futuro, promesa y paquete, podemos crear tareas simples sin preocuparnos demasiado por los hilos [el hilo es solo algo que damos para ejecutar una tarea].

Sin embargo, debemos considerar cuántos hilos usar o si una tarea se ejecuta mejor en el hilo actual o en otro, etc. Tales decisiones pueden ser manejadas por un lanzador de hilos llamado async(), que decide si crear un nuevo hilo o reciclar un viejo uno o simplemente ejecute la tarea en el hilo actual. Devuelve un futuro.

maxshuty
fuente
0

"La plantilla de clase std :: packaged_task envuelve cualquier objetivo invocable (función, expresión lambda, expresión de enlace u otro objeto de función) para que pueda invocarse de forma asincrónica. Su valor de retorno o excepción lanzada se almacena en un estado compartido al que se puede acceder a través de std :: objetos futuros ".

"La función de plantilla asíncrona ejecuta la función f de forma asincrónica (potencialmente en un hilo separado) y devuelve un std :: future que eventualmente contendrá el resultado de esa llamada de función".

Radoslav.B
fuente