¿Por qué std :: future es diferente devuelto de std :: packaged_task y std :: async?
Llegué a saber la razón por la que futureregresó std::asynctiene algún estado compartido especial a través del cual wait on returned futuresucedió en el destructor del futuro. Pero cuando lo usamos std::pakaged_task, su futuro no exhibe el mismo comportamiento. Para completar una tarea empaquetada, debe llamar explícitamente get()al futureobjeto desde packaged_task.
Ahora mis preguntas son:
- ¿Cuál podría ser la implementación interna del futuro (pensar
std::asyncvsstd::packaged_task)? - ¿Por qué no se aplicó el mismo comportamiento a los
futuredevueltosstd::packaged_task? O, en otras palabras, ¿cómo se detiene el mismo comportamientostd::packaged_taskfuture?
Para ver el contexto, consulte el código a continuación:
No espera para terminar countdowntarea. Sin embargo, si elimino los comentarios // int value = ret.get();, terminaría countdowny es obvio porque estamos literalmente bloqueando el futuro devuelto.
// packaged_task example
#include <iostream> // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for
// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Lift off!" <<std::endl;
return from-to;
}
int main ()
{
std::cout << "Start " << std::endl;
std::packaged_task<int(int,int)> tsk (countdown); // set up packaged_task
std::future<int> ret = tsk.get_future(); // get future
std::thread th (std::move(tsk),10,0); // spawn thread to count down from 10 to 0
// int value = ret.get(); // wait for the task to finish and get result
std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";
th.detach();
return 0;
}
Si uso std::asyncpara ejecutar una tarea countdownen otro hilo, no importa si lo uso get()en el futureobjeto devuelto o no , siempre terminará la tarea.
// packaged_task example
#include <iostream> // std::cout
#include <future> // std::packaged_task, std::future
#include <chrono> // std::chrono::seconds
#include <thread> // std::thread, std::this_thread::sleep_for
// count down taking a second for each value:
int countdown (int from, int to) {
for (int i=from; i!=to; --i) {
std::cout << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "Lift off!" <<std::endl;
return from-to;
}
int main ()
{
std::cout << "Start " << std::endl;
std::packaged_task<int(int,int)> tsk (countdown); // set up packaged_task
std::future<int> ret = tsk.get_future(); // get future
auto fut = std::async(std::move(tsk), 10, 0);
// int value = fut.get(); // wait for the task to finish and get result
std::cout << "The countdown lasted for " << std::endl;//<< value << " seconds.\n";
return 0;
}
Respuestas
std::asynctiene un conocimiento definido de cómo y dónde se ejecuta la tarea que se le asigna. Ese es su trabajo: ejecutar la tarea. Para hacer eso, tiene que ponerlo en alguna parte. Ese lugar podría ser un grupo de subprocesos, un subproceso recién creado o en un lugar para que lo ejecute quien destruya el archivo future.
Porque asyncsabe cómo se ejecutará la función, tiene el 100% de la información que necesita para construir un mecanismo que pueda comunicar cuándo ha concluido esa ejecución potencialmente asincrónica, así como para asegurarse de que si destruye el future, entonces cualquier mecanismo que vaya a ejecutar esa función eventualmente llegará a ejecutarla realmente. Después de todo, sabe cuál es ese mecanismo.
Pero packaged_task no lo hace . Todo lo que packaged_taskhace es almacenar un objeto invocable que se puede llamar con los argumentos dados, crear un promisecon el tipo del valor de retorno de la función y proporcionar un medio para obtener futurey ejecutar la función que genera el valor.
Cuándo y dónde se ejecuta realmente la tarea no packaged_taskes asunto de su incumbencia. Sin ese conocimiento, la sincronización necesaria para que futureel destructor se sincronice con la tarea simplemente no se puede construir.
Digamos que desea ejecutar la tarea en un hilo recién creado. Bien, entonces para sincronizar su ejecución con la futuredestrucción de ', necesitaría un mutex que bloqueará el destructor hasta que finalice el hilo de la tarea.
Pero, ¿qué sucede si desea ejecutar la tarea en el mismo hilo que el llamador del futuredestructor de? Bueno, entonces no puedes usar un mutex para sincronizar eso ya que todo está en el mismo hilo. En su lugar, debe hacer que el destructor invoque la tarea. Ese es un mecanismo completamente diferente y depende de cómo planee ejecutarlo.
Como packaged_taskno sabe cómo piensa ejecutarlo, no puede hacer nada de eso.
Tenga en cuenta que esto no es exclusivo de packaged_task. Todos los future vectores creados a partir de una creada por el usuario promiseobjeto no tendrá la propiedad especial de async's futures.
Entonces, la pregunta realmente debería ser por qué asyncfunciona de esta manera, no por qué todos los demás no lo hacen .
Si quiere saber eso, es debido a dos necesidades en competencia: asyncnecesitaba ser una forma simple de alto nivel y con muerte cerebral para obtener una ejecución asincrónica (para lo cual la sincronización en la destrucción tiene sentido), y nadie quería crear un nuevo futuretipo que era idéntico al existente salvo por el comportamiento de su destructor. Entonces decidieron sobrecargar cómo futurefunciona, complicando su implementación y uso.
@Nicol Bolas ya ha respondido a esta pregunta de manera bastante satisfactoria. Entonces intentaré responder la pregunta ligeramente desde una perspectiva diferente, elaborando los puntos ya mencionados por @Nicol Bolas.
El diseño de cosas relacionadas y sus objetivos.
Considere esta sencilla función que queremos ejecutar, de varias formas:
int add(int a, int b) {
std::cout << "adding: " << a << ", "<< b << std::endl;
return a + b;
}
Olvídese std::packaged_task, std ::futurey std::asyncpor un tiempo, retrocedamos un paso y revisemos cómo std::functionfunciona y qué problema causa.
caso 1: std::functionno es lo suficientemente bueno para ejecutar cosas en diferentes hilos
std::function<int(int,int)> f { add };
Una vez que lo tengamos f, podemos ejecutarlo, en el mismo hilo, como:
int result = f(1, 2); //note we can get the result here
O, en un hilo diferente, como este:
std::thread t { std::move(f), 3, 4 };
t.join();
Si miramos con atención, nos damos cuenta de que ejecutar fen un hilo diferente crea un nuevo problema: ¿cómo obtenemos el resultado de la función? Ejecutar fen el mismo hilo no tiene ese problema; obtenemos el resultado como valor devuelto, pero cuando lo ejecutamos en un hilo diferente, no tenemos ninguna forma de obtener el resultado. Eso es exactamente lo que resuelve std::packaged_task.
caso 2 - std::packaged_taskresuelve el problema que std::functionno resuelve
En particular, crea un canal entre los hilos para enviar el resultado al otro hilo. Aparte de eso, es más o menos lo mismo que std::function.
std::packaged_task<int(int,int)> f { add }; // almost same as before
std::future<int> channel = f.get_future(); // get the channel
std::thread t{ std::move(f), 30, 40 }; // same as before
t.join(); // same as before
int result = channel.get(); // problem solved: get the result from the channel
Ahora ves cómo std::packaged_taskresuelve el problema creado por std::function. Sin embargo, eso no significa que std::packaged_taskdeba ejecutarse en un hilo diferente. También puede ejecutarlo en el mismo hilo, igual que std::function, aunque seguirá obteniendo el resultado del canal.
std::packaged_task<int(int,int)> f { add }; // same as before
std::future<int> channel = f.get_future(); // same as before
f(10, 20); // execute it in the current thread !!
int result = channel.get(); // same as before
Entonces, fundamentalmente std::functiony std::packaged_taskson algo similar: simplemente envuelven la entidad invocable, con una diferencia: std::packaged_taskes compatible con múltiples subprocesos, porque proporciona un canal a través del cual puede pasar el resultado a otros subprocesos. Ambos NO ejecutan la entidad invocable envuelta por sí mismos. Es necesario invocarlos , ya sea en el mismo hilo o en otro hilo, para ejecutar la entidad invocable envuelta. Entonces, básicamente, hay dos tipos de cosas en este espacio:
- lo que se ejecuta es decir regular de funciones,
std::function,std::packaged_task, etc. - cómo / dónde se ejecuta, es decir, hilos, grupos de hilos, ejecutores, etc.
caso 3: std::asynces algo completamente diferente
Es algo diferente porque combina qué se ejecuta con cómo / dónde se ejecuta .
std::future<int> fut = std::async(add, 100, 200);
int result = fut.get();
Tenga en cuenta que en este caso, el futuro creado tiene un ejecutor asociado, lo que significa que el futuro se completará en algún momento ya que hay alguien ejecutando cosas detrás de la escena. Sin embargo, en el caso del futuro creado por std::packaged_task, no hay necesariamente un ejecutor y ese futuro puede que nunca se complete si la tarea creada nunca se entrega a ningún ejecutor.
Espero que te ayude a comprender cómo funcionan las cosas detrás de escena. Vea la demostración en línea .
La diferencia entre dos tipos de std::future
Bueno, en este punto, queda bastante claro que se std::futurepueden crear dos tipos :
- Un tipo puede ser creado por
std::async. Dicho futuro tiene un ejecutor asociado y por lo tanto puede completarse. - Otro tipo puede ser creado por
std::packaged_tasko cosas así. Dicho futuro no necesariamente tiene un ejecutor asociado y, por lo tanto, puede o no estar completo.
Dado que, en el segundo caso, el futuro no necesariamente tiene un ejecutor asociado, su destructor no está diseñado para su finalización / espera porque puede que nunca se complete:
{
std::packaged_task<int(int,int)> f { add };
std::future<int> fut = f.get_future();
} // fut goes out of scope, but there is no point
// in waiting in its destructor, as it cannot complete
// because as `f` is not given to any executor.
Espero que esta respuesta te ayude a entender las cosas desde una perspectiva diferente.
El cambio de comportamiento se debe a la diferencia entre std::thready std::async.
En el primer ejemplo, ha creado un subproceso de demonio separando. Donde imprime std::cout << "The countdown lasted for " << std::endl;en su hilo principal, puede ocurrir antes, durante o después de las declaraciones de impresión dentro de la countdownfunción del hilo. Debido a que el hilo principal no espera al hilo generado, es probable que ni siquiera vea todas las impresiones.
En el segundo ejemplo, inicia la función de hilo con la std::launch::deferredpolítica. El comportamiento de std :: async es:
Si se elige la política asincrónica, la finalización del subproceso asociado se sincroniza con el retorno exitoso de la primera función que está esperando en el estado compartido, o con el retorno de la última función que libera el estado compartido , lo que ocurra primero.
En este ejemplo, tiene dos futuros para el mismo estado compartido. Antes de que se llame a sus dtores al salir de main, la tarea asíncrona debe completarse. Incluso si no ha definido explícitamente ningún futuro, el futuro temporal que se crea y se destruye (devuelto desde la llamada a std::async) significará que la tarea se completa antes de que salga el hilo principal.
Aquí hay una excelente publicación de blog de Scott Meyers, que aclara el comportamiento de std::future& std::async.
Publicación SO relacionada .