c++ multithreading visual-studio memory-leaks

c++ - En Visual Studio, no se llama al destructor de las `thread_local` variables cuando se usa con std:: async, ¿es esto un error?



multithreading visual-studio (2)

El siguiente codigo

#include <iostream> #include <future> #include <thread> #include <mutex> std::mutex m; struct Foo { Foo() { std::unique_lock<std::mutex> lock{m}; std::cout <<"Foo Created in thread " <<std::this_thread::get_id() <<"/n"; } ~Foo() { std::unique_lock<std::mutex> lock{m}; std::cout <<"Foo Deleted in thread " <<std::this_thread::get_id() <<"/n"; } void proveMyExistance() { std::unique_lock<std::mutex> lock{m}; std::cout <<"Foo this = " << this <<"/n"; } }; int threadFunc() { static thread_local Foo some_thread_var; // Prove the variable initialized some_thread_var.proveMyExistance(); // The thread runs for some time std::this_thread::sleep_for(std::chrono::milliseconds{100}); return 1; } int main() { auto a1 = std::async(std::launch::async, threadFunc); auto a2 = std::async(std::launch::async, threadFunc); auto a3 = std::async(std::launch::async, threadFunc); a1.wait(); a2.wait(); a3.wait(); std::this_thread::sleep_for(std::chrono::milliseconds{1000}); return 0; }

Ancho compilado y ejecutado en macOS:

clang++ test.cpp -std=c++14 -pthread ./a.out

Tiene resultado

Foo Created in thread 0x70000d9f2000 Foo Created in thread 0x70000daf8000 Foo Created in thread 0x70000da75000 Foo this = 0x7fd871d00000 Foo this = 0x7fd871c02af0 Foo this = 0x7fd871e00000 Foo Deleted in thread 0x70000daf8000 Foo Deleted in thread 0x70000da75000 Foo Deleted in thread 0x70000d9f2000

Compilado y ejecutado en Visual Studio 2015 Update 3:

Foo Created in thread 7180 Foo this = 00000223B3344120 Foo Created in thread 8712 Foo this = 00000223B3346750 Foo Created in thread 11220 Foo this = 00000223B3347E60

Destructor no son llamados.

¿Es esto un error o alguna zona gris indefinida?

PD

Si el std::this_thread::sleep_for(std::chrono::milliseconds{1000}); sleep std::this_thread::sleep_for(std::chrono::milliseconds{1000}); al final no es lo suficientemente largo, es posible que no veas los 3 mensajes "Eliminar" a veces.

Cuando se utiliza std::thread lugar de std::async , se llama a los destructores en ambas plataformas y siempre se imprimen los 3 mensajes "Eliminar".


Parece que es otro de los muchos errores en VC ++. Considere esta cita del n4750

Todas las variables declaradas con la palabra clave thread_local tienen duración de almacenamiento de subprocesos. El almacenamiento para estas entidades durará la duración del hilo en el que se crean. Hay un objeto o referencia distinta por subproceso, y el uso del nombre declarado se refiere a la entidad asociada con el subproceso actual. 2 Una variable con duración de almacenamiento de subprocesos se inicializará antes de su primer uso (6.2) y, si se construye, se destruirá al salir del subproceso.

+ esto

Si la implementación elige la política launch :: async, - (5.3) una llamada a una función en espera en un objeto de retorno asíncrono que comparte el estado compartido creado por esta llamada asíncrona se bloqueará hasta que la hebra asociada haya finalizado, como si estuviera unida, o otro tiempo fuera (33.3.2.5);

Podría estar equivocado ("thread exit" vs "thread completed", pero creo que esto significa que las variables thread_local necesitan ser destruidas antes de que la llamada a .wait () se desbloquee.


Nota introductoria: ahora he aprendido mucho más sobre esto y, por lo tanto, he reescrito mi respuesta. Gracias a @super, @MM y (últimamente) @DavidHaim y @NoSenseEtAl por ponerme en el camino correcto.

La implementación de std::async Microsoft no es conforme, pero tienen sus razones y lo que han hecho puede ser útil, una vez que lo entiendas correctamente.

Para aquellos que no quieren eso, no es demasiado difícil codificar un reemplazo directo para std::async que funciona de la misma manera en todas las plataformas. He publicado uno here .

Edición: Guau, cuán abierta está la MS en estos días, me gusta, consulte: https://github.com/MicrosoftDocs/cpp-docs/issues/308

Vamos a estar al principio. cppreference tiene esto que decir (énfasis y tachado mío):

La función de plantilla async ejecuta la función f asincrónicamente ( potencialmente opcionalmente en un subproceso separado que puede ser parte de un grupo de subprocesos ).

Sin embargo, el estándar de C ++ dice esto:

Si launch::async está configurado en la policy , [ std::async ] llama [a la función f] como si estuviera en un nuevo hilo de ejecución ...

Entonces, ¿cuál es la correcta? Las dos afirmaciones tienen una semántica muy diferente como el OP ha descubierto. Bueno, por supuesto, el estándar es correcto, como muestran tanto clang como gcc, entonces, ¿por qué difiere la implementación de Windows? Y como tantas cosas, todo se reduce a la historia.

El enlace (antiguo) que MM dragó tiene esto que decir, entre otras cosas:

... Microsoft tiene su implementación de [ std::async ] en forma de PPL (Parallel Pattern Library) ... [y] puedo entender el entusiasmo de esas compañías por doblar las reglas y hacer accesibles estas bibliotecas a través de std::async , especialmente si pueden mejorar dramáticamente el rendimiento ...

... Microsoft quería cambiar la semántica de std::async cuando se le llama con launch_policy::async. Creo que esto quedó prácticamente descartado en la siguiente discusión ... (la razón es la siguiente: si desea saber más y leer el enlace, vale la pena).

Y PPL se basa en el soporte incorporado de Windows para ThreadPools , por lo que @super tenía razón.

Entonces, ¿qué hace el grupo de subprocesos de Windows y para qué sirve? Bueno, está pensado para administrar las tareas de ejecución rápida y de ejecución frecuente de manera eficiente, de modo que el punto 1 sea para no abusar de ellas , pero mis pruebas simples muestran que si este es su caso de uso, puede ofrecer eficiencias significativas. Lo hace, esencialmente, dos cosas.

  • Recicla hilos, en lugar de tener que iniciar siempre uno nuevo para cada tarea asíncrona que inicie.
  • Limita el número total de subprocesos en segundo plano que utiliza, después de lo cual se bloqueará una llamada a std::async hasta que un subproceso quede libre. En mi máquina, este número es 768.

Entonces, sabiendo todo eso, ahora podemos explicar las observaciones del OP:

  1. Se crea un nuevo hilo para cada una de las tres tareas iniciadas por main() (porque ninguna de ellas termina de inmediato).

  2. Cada uno de estos tres subprocesos crea una nueva variable local para subprocesos Foo some_thread_var .

  3. Estas tres tareas se ejecutan hasta el final, pero los subprocesos en los que se ejecutan permanecen en existencia (en reposo).

  4. Luego, el programa se duerme por un momento y luego sale, dejando las 3 variables de subprocesos locales sin destruir.

Realicé varias pruebas y, además, encontré algunas cosas clave:

  • Cuando se recicla un hilo, las variables locales del hilo se reutilizan. Específicamente, no se destruyen y luego se vuelven a crear (¡has sido advertido!).
  • Si todas las tareas asíncronas se completan y usted espera lo suficiente, la agrupación de hilos finaliza todos los hilos asociados y las variables locales de subprocesos se destruyen. (Sin duda, las reglas reales son más complejas que eso, pero eso es lo que observé).
  • A medida que se envían nuevas tareas asíncronas, el grupo de subprocesos limita la velocidad a la que se crean los nuevos subprocesos, con la esperanza de que uno sea libre antes de que necesite realizar todo ese trabajo (crear nuevos subprocesos es costoso). Por lo tanto, una llamada a std::async puede tardar un tiempo en volver (hasta 300 ms en mis pruebas). Mientras tanto, solo está dando vueltas, esperando que llegue su nave. Este comportamiento está documentado, pero lo llamo aquí en caso de que lo tome por sorpresa.

Conclusiones:

  1. La implementación de std::async Microsoft no es conforme, pero está claramente diseñada con un propósito específico, y ese propósito es hacer un buen uso de la API Win32 ThreadPool. Puede vencerlos por burlarse del estándar, pero ha sido así durante mucho tiempo y es probable que tengan (¡importante!) Clientes que confían en él. Les pediré que mencionen esto en su documentación. No hacerlo es criminal.

  2. No es seguro usar variables thread_local en tareas std::async en Windows. Simplemente no lo hagas, terminará en lágrimas.