c++ c++11 c++14 future

c++ - ¿Qué sucede cuando se reasigna a un futuro que aún no está listo?



c++11 c++14 (2)

Durante una revisión del código, encontré un fragmento de código que básicamente se reduce a esto:

#include <iostream> #include <future> #include <thread> int main( int, char ** ) { std::atomic<int> x( 0 ); std::future<void> task; for( std::size_t i = 0u; i < 5u; ++i ) { task = std::async( std::launch::async, [&x, i](){ std::this_thread::sleep_for( std::chrono::seconds( 2u * ( 5u - i ) ) ); ++x; } ); } task.get(); std::cout << x << std::endl; return 0; }

No estaba muy seguro de si

  • Se garantiza que todas las tareas se ejecutan al imprimir el resultado,
  • si las tareas se ejecutarían una tras otra (es decir, la asignación de tareas estaría bloqueando) o no.

No pude responder a esa pregunta leyendo la documentación en Internet, así que pensé en escribir el fragmento de arriba para averiguar qué hace realmente nuestro compilador.

Ahora, descubrí que la respuesta de lo que hace gcc-5 es indecisa y eso me hizo aún más curioso: se podría suponer que la asignación es de bloqueo o no de bloqueo.

Si está bloqueando, el tiempo tomado por el programa debería ser básicamente la suma del tiempo que las tareas individuales toman para ejecutarse. El primero toma 10 segundos, el segundo 8, el tercero 6, el cuarto 4 y los últimos 2 segundos. Así que en total debería tomar 10 + 8 + 6 + 4 + 2 = 30 segundos .

Si no es de bloqueo, debería llevar tanto tiempo como la última tarea, es decir, 2 segundos .

Esto es lo que sucede: Tarda 18 segundos (medido usando el tiempo ./a.out o un buen reloj antiguo). Al jugar un poco con el código, descubrí que el código se comporta como si la asignación fuera alternativamente bloqueada y no bloqueada.

Pero esto no puede ser verdad, ¿verdad? std::async probablemente vuelve a std::deferred mitad del tiempo? Mi depurador dice que genera dos subprocesos, se bloquea hasta que ambos subprocesos salen, luego genera dos subprocesos más y así sucesivamente.

¿Qué dice la norma? ¿Qué debería pasar? ¿Qué sucede dentro de gcc-5?


Se garantiza que todas las tareas se ejecutan al imprimir el resultado,

Solo se garantiza que la tarea asignada al último se haya ejecutado. Al menos, no pude encontrar ninguna regla que garantice el resto.

si las tareas se ejecutarían una tras otra (es decir, la asignación de tareas estaría bloqueando) o no.

La asignación de tareas generalmente no es de bloqueo, pero puede bloquearse en este caso, sin garantía.

[futures.unique_future]

future & operator = (future && rhs) noexcept;

  1. Efectos:

    libera cualquier estado compartido ([futures.state]).

[futures.state]

  1. Cuando se dice que un objeto de retorno asíncrono o un proveedor asíncrono libera su estado compartido, significa:

    • si el objeto o proveedor devuelto contiene la última referencia a su estado compartido, el estado compartido se destruye; y

    • el objeto o proveedor devuelto abandona su referencia a su estado compartido; y

    • estas acciones no se bloquearán para que el estado compartido esté listo, excepto que puede bloquearse si se cumplen todas las siguientes condiciones: el estado compartido fue creado por una llamada a std :: async, el estado compartido aún no está listo, y esto Fue la última referencia al estado compartido.

Todas las condiciones para el bloqueo potencial se cumplen en la tarea creada por std::async aún no se ha ejecutado.


En general, las asignaciones de la task través del operator=(&&) no tienen que estar bloqueando (ver más abajo), pero como creó std::future utilizando std::async , estas asignaciones se vuelven bloqueadas (gracias a @TC):

[future.async]

Si la implementación elige la política de lanzamiento :: async,

  • [...]

  • la finalización del hilo asociado sincroniza con ([intro.multithread]) el retorno de la primera función que detecta con éxito el estado listo del estado compartido o con el retorno de la última función que libera el estado compartido , lo que ocurra primero.

¿Por qué obtienes un tiempo de ejecución de 18 segundos?

Lo que sucede en su caso es que std::async inicia el "hilo" de su lambda antes de la asignación. Vea a continuación una explicación detallada de cómo obtiene un tiempo de ejecución de 18 segundos.

Esto es lo que (probablemente) sucede en su código ( e significa una epsilon):

  • t = 0 , primera llamada std::async con i = 0 , iniciando un nuevo hilo;
  • t = 0 + e , segunda llamada std::async con i = 1 iniciando un nuevo hilo, luego mueva. El movimiento liberará el estado de task compartido actual, bloqueando durante aproximadamente 10 segundos (pero el segundo std::async con i = 1 ya se está ejecutando);
  • t = 10 , tercera llamada std::async con i = 2 iniciando un nuevo hilo, luego mueva. El estado actual compartido de la task fue la llamada con i = 1 que ya está lista, por lo que nada bloquea;
  • t = 10 + e , cuarta llamada std::async con i = 3 iniciando un nuevo hilo, luego mueva. El movimiento está bloqueando porque el std::async con i = 2 no está listo pero el hilo para i = 3 ya comenzó;
  • t = 16 , fifth std::async call con i = 4 iniciando un nuevo hilo, luego mueva. El estado compartido actual de la task ( i = 3 ) ya está listo, por lo que no está bloqueado;
  • t = 16 + e , fuera de los bucles, llame a .get() espere a que el * estado compartido` esté listo;
  • t = 18 , el estado compartido se prepara para que todo termine.

Detalles estándar en std::future::operator= :

Aquí está la cita estándar para operator= on std::future :

future& operator=(future&& rhs) noexcept;

Efectos:

  • (10.1) - libera cualquier estado compartido (30.6.4).
  • ...

Y aquí es lo que significa "Libera cualquier estado compartido" (el énfasis es mío):

Cuando se dice que un objeto de retorno asíncrono o un proveedor asíncrono libera su estado compartido, significa:

(5.1) - [...]

(5.2) - [...]

(5.3): estas acciones no se bloquearán para que el estado compartido esté listo, excepto que puede bloquearse si se cumplen todas las siguientes condiciones: el estado compartido fue creado por una llamada a std :: async, el estado compartido aún no está Listo, y esta fue la última referencia al estado compartido .

Tu caso cae en lo que yo enfatizaba (creo). Creó el estado compartido usando std::async , está inactivo (por lo que no está listo) y usted solo tiene una referencia a él, por lo que esto puede estar bloqueando.