¿Qué le sucede a un hilo separado cuando sale main ()?

152

Supongamos que estoy iniciando un std::thready luego detach(), por lo que el hilo continúa ejecutándose a pesar de std::threadque el que una vez lo representó, queda fuera de alcance.

Suponga además que el programa no tiene un protocolo confiable para unir el hilo separado 1 , por lo que el hilo separado aún se ejecuta cuando main()sale.

No puedo encontrar nada en el estándar (más precisamente, en el borrador N3797 C ++ 14), que describe lo que debería suceder, ni 1.10 ni 30.3 contienen palabras pertinentes.

1 Otra pregunta, probablemente equivalente, es: "¿se puede volver a unir un subproceso separado?", Ya que sea cual sea el protocolo al que se va a unir, la parte de señalización debe realizarse mientras el subproceso todavía se está ejecutando, y el programador del sistema operativo puede decida poner el hilo en reposo durante una hora justo después de que se haya realizado la señalización, sin que el receptor pueda detectar de manera confiable que el hilo realmente terminó.

Si quedarse sin main()hilos desconectados en ejecución es un comportamiento indefinido, entonces cualquier uso de este std::thread::detach()es un comportamiento indefinido a menos que el hilo principal nunca salga 2 .

Por lo tanto, quedarse sin main()hilos separados que se ejecutan debe tener efectos definidos . La pregunta es: dónde (en el estándar C ++ , no POSIX, no documentos del sistema operativo, ...) se definen esos efectos.

2 No se puede unir un hilo separado (en el sentido de std::thread::join()). Usted puede esperar los resultados de las discusiones separadas (por ejemplo a través de un futuro a partir de std::packaged_task, o por un semáforo contador o una bandera y una variable de condición), pero eso no garantiza que el mensaje ha terminado de ejecutarse . De hecho, a menos que poner la parte de señalización en el destructor del primer objeto automático del hilo, no será , en general, ser de código (destructores) que se ejecutan después de que el código de señalización. Si el sistema operativo programa el hilo principal para consumir el resultado y salir antes de que el hilo separado termine de ejecutar dichos destructores, ¿qué definirá ^ Wis para que suceda?

Marc Mutz - mmutz
fuente
55
Solo puedo encontrar una nota no obligatoria muy vaga en [basic.start.term] / 4: "Terminar cada hilo antes de una llamada std::exito salir de maines suficiente, pero no necesario, para satisfacer estos requisitos". (todo el párrafo puede ser relevante) Consulte también [support.start.term] / 8 ( std::exitse llama cuando se maindevuelve)
dyp

Respuestas:

45

La respuesta a la pregunta original "qué sucede con un hilo separado cuando main()sale" es:

Continúa ejecutándose (porque el estándar no dice que está detenido), y eso está bien definido, siempre que no toque ni las variables (automáticas | thread_local) de otros hilos ni los objetos estáticos.

Esto parece estar permitido para permitir administradores de subprocesos como objetos estáticos (nota en [basic.start.term] / 4 lo dice, gracias a @dyp por el puntero).

Los problemas surgen cuando la destrucción de los objetos estáticos ha finalizado, porque la ejecución entra en un régimen en el que solo se puede ejecutar el código permitido en los manejadores de señales ( [basic.start.term] / 1, primera oración ). De la biblioteca estándar de C ++, esa es solo la <atomic>biblioteca ( [support.runtime] / 9, segunda oración ). En particular, eso, en general, excluye condition_variable (está definido por la implementación si se guarda para usar en un controlador de señal, porque no es parte de <atomic>).

A menos que haya desenrollado su pila en este punto, es difícil ver cómo evitar un comportamiento indefinido.

La respuesta a la segunda pregunta "¿se pueden volver a unir hilos separados?" Es:

Sí, con la *_at_thread_exitfamilia de funciones ( notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit(), ...).

Como se señaló en la nota de pie de página [2] de la pregunta, señalar una variable de condición o un semáforo o un contador atómico no es suficiente para unir un hilo separado (en el sentido de asegurar que el final de su ejecución ha sucedido antes de la recepción de dicha señalización por un subproceso en espera), porque, en general, habrá más código ejecutado después, por ejemplo, notify_all()de una variable de condición, en particular los destructores de objetos automáticos y locales de subprocesos.

Ejecutar la señalización como lo último que hace el subproceso ( después de que ocurrieron destructores de objetos automáticos y locales de subprocesos ) es para lo que _at_thread_exitse diseñó la familia de funciones.

Por lo tanto, para evitar un comportamiento indefinido en ausencia de garantías de implementación por encima de lo que requiere el estándar, debe unir (manualmente) un hilo separado con una _at_thread_exitfunción que haga la señalización o hacer que el hilo separado ejecute solo un código que sea seguro para un manejador de señal, también.

Marc Mutz - mmutz
fuente
17
¿Estas seguro acerca de esto? En todas partes que probé (GCC 5, clang 3.5, MSVC 14), todos los hilos separados se eliminan cuando sale el hilo principal.
rustyx
3
Creo que el problema no es lo que hace una implementación específica, sino cómo evitar lo que el estándar define como comportamiento indefinido.
Jon Spencer
77
Esta respuesta parece implicar que después de la destrucción de variables estáticas, el proceso entrará en algún tipo de estado de suspensión esperando que finalicen los subprocesos restantes. Eso no es cierto, después de exitterminar de destruir objetos estáticos, ejecutar atexitcontroladores, vaciar secuencias, etc. devuelve el control al entorno host, es decir, el proceso se cierra. Si un hilo separado todavía se está ejecutando (y de alguna manera ha evitado un comportamiento indefinido al no tocar nada fuera de su propio hilo), simplemente desaparece en una nube de humo a medida que el proceso sale.
Jonathan Wakely
3
Si está de acuerdo con el uso de API C ++ que no son ISO, entonces, si las mainllamadas en pthread_exitlugar de regresar o llamar exit, eso hará que el proceso espere a que finalicen los hilos desconectados y luego llame exitdespués de que finalice el último.
Jonathan Wakely
3
"Continúa ejecutándose (porque el estándar no dice que está detenido)" -> ¿Alguien puede decirme CÓMO un hilo puede continuar la ejecución sin su proceso contenedor?
Gupta
42

Separar hilos

De acuerdo a std::thread::detach:

Separa el hilo de ejecución del objeto de hilo, permitiendo que la ejecución continúe independientemente. Cualquier recurso asignado se liberará una vez que salga el hilo.

De pthread_detach:

La función pthread_detach () indicará a la implementación que el almacenamiento para el subproceso puede recuperarse cuando ese subproceso finaliza. Si el hilo no ha terminado, pthread_detach () no hará que termine. El efecto de múltiples llamadas pthread_detach () en el mismo hilo objetivo no está especificado.

Separar hilos es principalmente para ahorrar recursos, en caso de que la aplicación no necesite esperar a que termine un hilo (por ejemplo, demonios, que deben ejecutarse hasta la finalización del proceso):

  1. Para liberar el controlador del lado de la aplicación: se puede dejar que un std::threadobjeto quede fuera del alcance sin unirse, lo que normalmente lleva a una llamada a la std::terminate()destrucción.
  2. Para permitir que el sistema operativo limpie los recursos específicos del hilo ( TCB ) automáticamente tan pronto como el hilo salga, porque especificamos explícitamente, que no estamos interesados ​​en unir el hilo más adelante, por lo tanto, uno no puede unirse a un hilo ya separado.

Matar hilos

El comportamiento en la terminación del proceso es el mismo que el del subproceso principal, que al menos podría captar algunas señales. No es tan importante si otros hilos pueden manejar señales o no, ya que uno podría unir o terminar otros hilos dentro de la invocación del controlador de señal del hilo principal. (Pregunta relacionada )

Como ya se dijo, cualquier hilo, ya sea separado o no, morirá con su proceso en la mayoría de los sistemas operativos . El proceso en sí mismo puede terminarse levantando una señal, llamando exit()o volviendo de la función principal. Sin embargo, C ++ 11 no puede y no intenta definir el comportamiento exacto del sistema operativo subyacente, mientras que los desarrolladores de una máquina virtual Java seguramente pueden abstraer tales diferencias hasta cierto punto. AFAIK, los procesos exóticos y los modelos de subprocesos generalmente se encuentran en plataformas antiguas (a las que C ++ 11 probablemente no se portará) y en varios sistemas integrados, que podrían tener una implementación de biblioteca de idiomas especial y / o limitada y también soporte de idiomas limitado.

Soporte de hilo

Si los subprocesos no son compatibles, std::thread::get_id()debería devolver una identificación no válida (construcción predeterminada std::thread::id) ya que hay un proceso simple, que no necesita un objeto de subproceso para ejecutarse y el constructor de a std::threaddebería arrojar un std::system_error. Así es como entiendo C ++ 11 junto con los sistemas operativos actuales. Si hay un sistema operativo con soporte para subprocesos, que no genera un subproceso principal en sus procesos, avíseme.

Hilos de control

Si uno necesita mantener el control sobre un subproceso para un apagado correcto, puede hacerlo utilizando primitivas de sincronización y / o algún tipo de indicadores. Sin embargo, en este caso, configurar un indicador de apagado seguido de una unión es la forma que prefiero, ya que no tiene sentido aumentar la complejidad al separar los hilos, ya que los recursos se liberarían al mismo tiempo de todos modos, donde los pocos bytes del std::threadobjeto vs. mayor complejidad y posiblemente más primitivas de sincronización deberían ser aceptables.

Sam
fuente
3
Dado que cada subproceso tiene su propia pila (que está en el rango de megabytes en Linux), elegiría separar el subproceso (por lo que su pila se liberará tan pronto como salga) y usar algunas primitivas de sincronización si el subproceso principal necesita salir (y para un apagado correcto, necesita unir los subprocesos aún en ejecución en lugar de terminarlos al regresar / salir).
Norbert Bérci
8
Realmente no veo cómo esto responde la pregunta
MikeMB
18

Considere el siguiente código:

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void thread_fn() {
  std::this_thread::sleep_for (std::chrono::seconds(1)); 
  std::cout << "Inside thread function\n";   
}

int main()
{
    std::thread t1(thread_fn);
    t1.detach();

    return 0; 
}

Al ejecutarlo en un sistema Linux, el mensaje de thread_fn nunca se imprime. El sistema operativo se limpia thread_fn()tan pronto como main()sale. Reemplazar t1.detach()con t1.join()siempre imprime el mensaje como se esperaba.

kgvinod
fuente
Este comportamiento ocurre exactamente en Windows. Entonces, parece que Windows mata los hilos separados cuando el programa finaliza.
Gupta
17

El destino del hilo después de que el programa sale es un comportamiento indefinido. Pero un sistema operativo moderno limpiará todos los hilos creados por el proceso al cerrarlo.

Al separar un std::thread, estas tres condiciones continuarán siendo válidas:

  1. *this ya no posee ningún hilo
  2. joinable() siempre será igual a false
  3. get_id() será igual std::thread::id()
César
fuente
1
¿Por qué indefinido? ¿Porque el estándar no define nada? Según mi nota al pie de página, ¿no haría eso un llamado a detach()tener un comportamiento indefinido? Difícil de creer ...
Marc Mutz - mmutz
2
@ MarcMutz-mmutz No está definido en el sentido de que si el proceso finaliza, el destino del hilo no está definido.
César el
2
@ Caesar y ¿cómo me aseguro de no salir antes de que finalice el hilo?
MichalH
0

Para permitir que otros hilos continúen la ejecución, el hilo principal debe terminar llamando a pthread_exit () en lugar de salir (3). Está bien usar pthread_exit en main. Cuando se usa pthread_exit, el subproceso principal dejará de ejecutarse y permanecerá en estado zombie (desactivado) hasta que todos los otros subprocesos salgan. Si está utilizando pthread_exit en el subproceso principal, no puede obtener el estado de retorno de otros subprocesos y no puede realizar la limpieza de otros subprocesos (podría hacerse usando pthread_join (3)). Además, es mejor separar los hilos (pthread_detach (3)) para que los recursos del hilo se liberen automáticamente en la terminación del hilo. Los recursos compartidos no se liberarán hasta que salgan todos los hilos.

yshi
fuente
@kgvinod, ¿por qué no agregar "pthread_exit (0);" después del "ti.detach ()";
yshi