Qu'arrive-t-il à un thread détaché lorsque main () se ferme?

153

Supposons que je démarre un std::threadet puis detach()il, donc le thread continue à s'exécuter même si celui std::threadqui l'a représenté une fois, sort de la portée.

Supposons en outre que le programme ne dispose pas d'un protocole fiable pour joindre le thread détaché 1 , de sorte que le thread détaché s'exécute toujours lorsqu'il se main()termine.

Je ne trouve rien dans la norme (plus précisément, dans le projet N3797 C ++ 14), qui décrit ce qui devrait se passer, ni 1.10 ni 30.3 ne contiennent un libellé pertinent.

1 Une autre question, probablement équivalente, est la suivante: "un thread détaché peut-il être joint à nouveau", car quel que soit le protocole que vous inventez pour le rejoindre, la partie de signalisation devrait être Décider de mettre le thread en veille pendant une heure juste après que la signalisation a été effectuée sans aucun moyen pour l'extrémité de réception de détecter de manière fiable que le thread est effectivement terminé.

Si le fait de manquer de main()threads détachés en cours d'exécution n'est pas un comportement défini, alors toute utilisation de std::thread::detach()est un comportement indéfini à moins que le thread principal ne se termine jamais 2 .

Ainsi, manquer de main()threads détachés en cours d'exécution doit avoir des effets définis . La question est: (dans le standard C ++ , pas POSIX, pas de documentation OS, ...) sont définis ces effets.

2 Un fil détaché ne peut pas être joint (au sens de std::thread::join()). Vous pouvez attendre les résultats des threads détachés (par exemple via un futur from std::packaged_task, ou par un sémaphore de comptage ou un drapeau et une variable de condition), mais cela ne garantit pas que le thread a fini de s'exécuter . En effet, à moins que vous ne mettiez la partie signalisation dans le destructeur du premier objet automatique du thread, il y aura , en général, du code (destructeurs) qui s'exécutera après le code de signalisation. Si le système d'exploitation planifie le thread principal pour qu'il consomme le résultat et se termine avant que le thread détaché ait fini d'exécuter lesdits destructeurs, qu'est-ce que ^ Sera défini pour se produire?

Marc Mutz - mmutz
la source
5
Je ne peux trouver qu'une très vague note non obligatoire dans [basic.start.term] / 4: "Terminer chaque thread avant un appel std::exitou la sortie de mainest suffisant, mais pas nécessaire, pour satisfaire ces exigences." (le paragraphe entier peut être pertinent) Voir aussi [support.start.term] / 8 ( std::exitest appelé au mainretour)
dyp

Réponses:

45

La réponse à la question initiale "qu'arrive-t-il à un thread détaché à la main()sortie" est:

Il continue de fonctionner (car le standard ne dit pas qu'il est arrêté), et c'est bien défini, tant qu'il ne touche ni les variables (automatic | thread_local) des autres threads ni les objets statiques.

Cela semble être autorisé à autoriser les gestionnaires de threads en tant qu'objets statiques (la note dans [basic.start.term] / 4 en dit long, grâce à @dyp pour le pointeur).

Des problèmes surviennent lorsque la destruction des objets statiques est terminée, car alors l'exécution entre dans un régime où seul le code autorisé dans les gestionnaires de signaux peut s'exécuter ( [basic.start.term] / 1, 1ère phrase ). De la bibliothèque standard C ++, c'est seulement la <atomic>bibliothèque ( [support.runtime] / 9, 2ème phrase ). En particulier, cela - en général - exclut condition_variable (il est défini par l'implémentation si cela est sauvegardé pour être utilisé dans un gestionnaire de signaux, car il n'en fait pas partie <atomic>).

Sauf si vous avez déroulé votre pile à ce stade, il est difficile de voir comment éviter un comportement indéfini.

La réponse à la deuxième question «les threads détachés peuvent-ils être rejoints» est:

Oui, avec la *_at_thread_exitfamille de fonctions ( notify_all_at_thread_exit(), std::promise::set_value_at_thread_exit()...).

Comme noté dans la note de bas de page [2] de la question, signaler une variable de condition ou un sémaphore ou un compteur atomique n'est pas suffisant pour rejoindre un thread détaché (dans le sens de s'assurer que la fin de son exécution s'est produite-avant la réception de ladite signalisation par un thread en attente), car, en général, il y aura plus de code exécuté après par exemple une notify_all()d'une variable de condition, en particulier les destructeurs d'objets automatiques et thread-locaux.

Exécuter la signalisation comme la dernière chose que fait le thread ( après que les destructeurs d'objets automatiques et locaux du thread se soient produits ) est ce pour quoi la _at_thread_exitfamille de fonctions a été conçue.

Ainsi, afin d'éviter un comportement indéfini en l'absence de garanties d'implémentation supérieures à ce que la norme exige, vous devez (manuellement) rejoindre un thread détaché avec une _at_thread_exitfonction faisant la signalisation ou faire en sorte que le thread détaché exécute uniquement du code qui serait sûr pour un gestionnaire de signaux, aussi.

Marc Mutz - mmutz
la source
17
Es-tu sûr de ça? Partout où j'ai testé (GCC 5, clang 3.5, MSVC 14), tous les threads détachés sont tués lorsque le thread principal se termine.
rustyx
3
Je pense que la question n'est pas de savoir ce que fait une mise en œuvre spécifique, mais de savoir comment éviter ce que la norme définit comme un comportement indéfini.
Jon Spencer
7
Cette réponse semble impliquer qu'après la destruction des variables statiques, le processus passera dans une sorte d'état de veille en attendant la fin des threads restants. Ce n'est pas vrai, après avoir exitfini de détruire des objets statiques, d'exécuter des atexitgestionnaires, de purger des flux, etc., il retourne le contrôle à l'environnement hôte, c'est-à-dire que le processus se termine . Si un thread détaché est toujours en cours d'exécution (et a en quelque sorte évité un comportement indéfini en ne touchant rien en dehors de son propre thread), il disparaît alors dans une bouffée de fumée lorsque le processus se termine.
Jonathan Wakely
3
Si vous êtes d'accord pour utiliser des API C ++ non ISO, si des mainappels pthread_exitau lieu de renvoyer ou d'appeler, exitle processus attendra la fin des threads détachés, puis l'appel une exitfois le dernier terminé.
Jonathan Wakely
3
"Il continue de fonctionner (parce que le standard ne dit pas qu'il est arrêté)" -> Quelqu'un peut-il me dire COMMENT un thread peut continuer à s'exécuter sans son processus de conteneur?
Gupta
42

Détacher des threads

Selon std::thread::detach:

Sépare le thread d'exécution de l'objet thread, ce qui permet à l'exécution de se poursuivre indépendamment. Toutes les ressources allouées seront libérées une fois le thread terminé.

De pthread_detach:

La fonction pthread_detach () doit indiquer à l'implémentation que le stockage du thread peut être récupéré lorsque ce thread se termine. Si le thread ne s'est pas terminé, pthread_detach () ne le fera pas se terminer. L'effet de plusieurs appels pthread_detach () sur le même thread cible n'est pas spécifié.

La dissociation des threads sert principalement à économiser des ressources, au cas où l'application n'aurait pas besoin d'attendre la fin d'un thread (par exemple, les démons, qui doivent s'exécuter jusqu'à la fin du processus):

  1. Pour libérer le handle côté application: On peut laisser un std::threadobjet sortir de sa portée sans le rejoindre, ce qui conduit normalement à un appel à la std::terminate()destruction.
  2. Pour permettre au système d'exploitation de nettoyer automatiquement les ressources spécifiques au thread ( TCB ) dès que le thread se termine, car nous avons explicitement spécifié que nous ne sommes pas intéressés à rejoindre le thread plus tard, par conséquent, on ne peut pas rejoindre un thread déjà détaché.

Tuer des fils

Le comportement à la fin du processus est le même que celui du thread principal, qui pourrait au moins capter certains signaux. Que d'autres threads puissent ou non gérer les signaux n'est pas si important, car on pourrait rejoindre ou terminer d'autres threads dans l'appel du gestionnaire de signaux du thread principal. (Question connexe )

Comme déjà indiqué, tout thread, qu'il soit détaché ou non, mourra avec son processus sur la plupart des systèmes d'exploitation . Le processus lui-même peut être terminé en élevant un signal, en appelant exit()ou en revenant de la fonction principale. Cependant, C ++ 11 ne peut pas et n'essaye pas de définir le comportement exact du système d'exploitation sous-jacent, alors que les développeurs d'une machine virtuelle Java peuvent sûrement résumer ces différences dans une certaine mesure. AFAIK, les modèles de processus et de threads exotiques se trouvent généralement sur des plates-formes anciennes (sur lesquelles C ++ 11 ne sera probablement pas porté) et sur divers systèmes embarqués, qui pourraient avoir une implémentation de bibliothèque de langage spéciale et / ou limitée et également un support de langage limité.

Prise en charge des threads

Si les threads ne sont pas pris en charge, il std::thread::get_id()doit renvoyer un identifiant non valide (construit par défaut std::thread::id) car il existe un processus simple, qui n'a pas besoin d'un objet thread pour s'exécuter et le constructeur de a std::threaddoit lancer un std::system_error. C'est ainsi que je comprends C ++ 11 en conjonction avec les systèmes d'exploitation actuels. S'il existe un système d'exploitation avec prise en charge des threads, qui ne génère pas de thread principal dans ses processus, faites-le moi savoir.

Contrôle des threads

Si l'on a besoin de garder le contrôle sur un thread pour un arrêt correct, on peut le faire en utilisant des primitives de synchronisation et / ou une sorte d'indicateur. Cependant, dans ce cas, définir un indicateur d'arrêt suivi d'une jointure est la façon dont je préfère, car il ne sert à rien d'augmenter la complexité en détachant les threads, car les ressources seraient libérées en même temps de toute façon, là où les quelques octets de l' std::threadobjet vs une complexité plus élevée et peut-être plus de primitives de synchronisation devraient être acceptables.

Sam
la source
3
Étant donné que chaque thread a sa propre pile (qui est dans la plage des mégaoctets sous Linux), je choisirais de détacher le thread (de sorte que sa pile sera libérée dès sa sortie) et d'utiliser des primitives de synchronisation si le thread principal doit quitter (et pour un arrêt correct, il doit rejoindre les threads toujours en cours d'exécution au lieu de les terminer au retour / à la sortie).
Norbert Bérci
8
Je ne vois vraiment pas comment cela répond à la question
MikeMB
18

Considérez le code suivant:

#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; 
}

En l'exécutant sur un système Linux, le message de thread_fn n'est jamais imprimé. L'OS nettoie en effet thread_fn()dès qu'il main()sort. Le remplacement t1.detach()par t1.join()imprime toujours le message comme prévu.

kgvinod
la source
Ce comportement se produit exactement sous Windows. Ainsi, il semble que Windows tue les threads détachés lorsque le programme est terminé.
Gupta
17

Le sort du thread après la fermeture du programme est un comportement indéfini. Mais un système d'exploitation moderne nettoiera tous les threads créés par le processus lors de sa fermeture.

Lors du détachement d'un std::thread, ces trois conditions continueront de s'appliquer:

  1. *this ne possède plus de thread
  2. joinable() sera toujours égal à false
  3. get_id() sera égal std::thread::id()
César
la source
1
Pourquoi indéfini? Parce que la norme ne définit rien? Par ma note de bas de page, cela ne ferait-il pas appel à detach()un comportement indéfini? Difficile à croire ...
Marc Mutz - mmutz
2
@ MarcMutz-mmutz Il est indéfini dans le sens où si le processus se termine, le sort du thread n'est pas défini.
Caesar
2
@Caesar et comment m'assurer de ne pas quitter avant la fin du thread?
MichalH
0

Pour permettre à d'autres threads de continuer l'exécution, le thread principal doit se terminer en appelant pthread_exit () plutôt que exit (3). C'est bien d'utiliser pthread_exit dans main. Lorsque pthread_exit est utilisé, le thread principal s'arrêtera de s'exécuter et restera à l'état zombie (défunt) jusqu'à ce que tous les autres threads se terminent. Si vous utilisez pthread_exit dans le thread principal, ne pouvez pas obtenir le statut de retour des autres threads et ne pouvez pas faire de nettoyage pour les autres threads (cela pourrait être fait en utilisant pthread_join (3)). De plus, il est préférable de détacher les threads (pthread_detach (3)) afin que les ressources de thread soient automatiquement libérées à la fin du thread. Les ressources partagées ne seront pas libérées tant que tous les threads ne seront pas fermés.

yshi
la source
@kgvinod, pourquoi ne pas ajouter "pthread_exit (0);" après le "ti.detach ()";
yshi