¿Qué sucede con un hilo desconectado cuando sale main()?
Supongamos que estoy iniciando a std::thread
y luego detach()
, de modo que el hilo continúa ejecutándose aunque el std::thread
que una vez lo representó esté fuera de alcance.
Supongamos además que el programa no tiene un protocolo confiable para unirse al subproceso separado 1 , por lo que el subproceso 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 describa lo que debería suceder, ni 1.10 ni 30.3 contienen la redacción pertinente.
1 Otra pregunta, probablemente equivalente, es: "¿se puede volver a unir un subproceso desconectado?", porque sea cual sea el protocolo que esté inventando para unirse, la parte de señalización tendría que realizarse mientras el subproceso aún se estaba ejecutando, y el programador del sistema operativo podría decide poner el hilo en suspensión durante una hora justo después de que se realizó la señalización sin que el extremo receptor pueda detectar de manera confiable que el hilo realmente terminó.
Si quedarse sin main()
subprocesos separados en ejecución es un comportamiento indefinido, entonces cualquier uso de std::thread::detach()
es un comportamiento indefinido a menos que el subproceso principal nunca salga 2 .
Por lo tanto, quedarse sin main()
subprocesos separados en ejecución debe tener efectos definidos . La pregunta es: dónde (en el estándar C++ , no en POSIX, no en los documentos del sistema operativo, ...) se definen esos efectos.
2 Un hilo desprendido no se puede unir (en el sentido de std::thread::join()
). Puede esperar los resultados de los subprocesos desconectados (por ejemplo, a través de un futuro desde o mediante un semáforo de conteo o una bandera y unastd::packaged_task
variable de condición), pero eso no garantiza que el subproceso haya terminado de ejecutarse . De hecho, a menos que coloque la parte de señalización en el destructor del primer objeto automático del hilo, habrá , en general, código (destructores) que se ejecutará después del 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 desconectado termine de ejecutar dichos destructores, ¿qué pasará?
La respuesta a la pregunta original "qué sucede con un hilo desconectado cuando main()
sale" es:
Continúa ejecutándose (porque el estándar no dice que esté detenido), y eso está bien definido, siempre y cuando no toque variables (automáticas|thread_local) de otros subprocesos ni objetos estáticos.
Parece que esto permite permitir administradores de subprocesos como objetos estáticos (la nota en [basic.start.term]/4 dice lo mismo, gracias a @dyp por el puntero).
Los problemas surgen cuando la destrucción de objetos estáticos ha finalizado, porque entonces la ejecución entra en un régimen en el que sólo se puede ejecutar el código permitido en los manejadores de señales ( [basic.start.term]/1, 1ª frase ). 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 usarlo en un controlador de señales, 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 los hilos separados?" es:
Sí, con la *_at_thread_exit
familia de funciones ( notify_all_at_thread_exit()
, std::promise::set_value_at_thread_exit()
, ...).
Como se señala en la nota al pie [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 garantizar que el final de su ejecución haya ocurrido antes de la recepción de dicha señalización por un hilo en espera), porque, en general, habrá más código ejecutado después de, por ejemplo, notify_all()
una variable de condición, en particular los destructores de objetos automáticos y locales de hilo.
Ejecutar la señalización como lo último que hace el hilo ( después de que hayan sucedido los destructores de objetos automáticos y locales de hilo ) es _at_thread_exit
para lo que se diseñó la familia de funciones.
Por lo tanto, para evitar un comportamiento indefinido en ausencia de garantías de implementación superiores a las que requiere el estándar, debe unir (manualmente) un subproceso separado con una _at_thread_exit
función que realiza la señalización o hacer que el subproceso separado ejecute solo código que sea seguro para un manejador de señales también.
Separar hilos
De acuerdo a std::thread::detach
:
Separa el hilo de ejecución del objeto del hilo, permitiendo que la ejecución continúe de forma independiente. Todos los recursos asignados se liberarán 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 se puede recuperar cuando ese subproceso finalice. Si el hilo no ha terminado, pthread_detach() no hará que termine. El efecto de múltiples llamadas pthread_detach() en el mismo hilo de destino no se especifica.
La separación de subprocesos sirve principalmente para ahorrar recursos, en caso de que la aplicación no necesite esperar a que finalice un subproceso (por ejemplo, demonios, que deben ejecutarse hasta la finalización del proceso):
- Para liberar el identificador del lado de la aplicación: se puede dejar que un
std::thread
objeto salga del alcance sin unirse, lo que normalmente conduce a una llamada astd::terminate()
la destrucción. - 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 unirnos al hilo más adelante, por lo que no se puede unir a un hilo ya desconectado.
Hilos asesinos
El comportamiento al finalizar el proceso es el mismo que el del hilo principal, que al menos podría captar algunas señales. No es tan importante si otros subprocesos pueden manejar señales o no, ya que uno podría unir o terminar otros subprocesos dentro de la invocación del controlador de señales del subproceso principal. (Pregunta relacionada )
Como ya se indicó, cualquier hilo, ya sea desconectado o no, morirá con su proceso en la mayoría de los sistemas operativos . El proceso en sí se puede finalizar generando una señal, llamando exit()
o regresando de la función principal. Sin embargo, C++ 11 no puede ni intenta definir el comportamiento exacto del sistema operativo subyacente, mientras que los desarrolladores de una VM Java seguramente pueden abstraer tales diferencias hasta cierto punto. AFAIK, los modelos de procesos y subprocesos exóticos generalmente se encuentran en plataformas antiguas (a las que probablemente no se migrará C++ 11) y en varios sistemas integrados, que podrían tener una implementación de biblioteca de idiomas especial y/o limitada y también un soporte de idiomas limitado.
Soporte de hilo
Si los subprocesos no son compatibles, std::thread::get_id()
se debe devolver una identificación no válida (construida por defecto std::thread::id
), ya que hay un proceso simple, que no necesita un objeto de subproceso para ejecutarse y el constructor de a std::thread
debe generar un std::system_error
. Así es como entiendo C++ 11 en relación con los sistemas operativos actuales. Si hay un sistema operativo compatible con subprocesos que no genera un subproceso principal en sus procesos, hágamelo saber.
Controlar hilos
Si uno necesita mantener el control sobre un hilo para un cierre adecuado, puede hacerlo usando primitivas de sincronización y/o algún tipo de indicador. Sin embargo, en este caso, la forma que prefiero es establecer un indicador de apagado seguido de una unión, ya que no tiene sentido aumentar la complejidad separando subprocesos, ya que los recursos se liberarían al mismo tiempo de todos modos, donde los pocos bytes del std::thread
objeto vs. una mayor complejidad y posiblemente más primitivas de sincronización deberían ser aceptables.
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. De hecho, el sistema operativo se limpia thread_fn()
tan pronto como main()
se cierra. Reemplazar t1.detach()
con t1.join()
siempre imprime el mensaje como se esperaba.
El destino del hilo después de que sale el programa 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 seguirán siendo válidas:
*this
ya no posee ningún hilojoinable()
siempre será igual afalse
get_id()
será igualstd::thread::id()