35 votes

Comment quitter proprement un programme C++ à threads ?

Je crée plusieurs threads dans mon programme. En appuyant sur Ctrl-C, un gestionnaire de signaux est appelé. À l'intérieur d'un gestionnaire de signaux, j'ai mis exit(0) en dernier. Le problème est que parfois le programme se termine en toute sécurité mais d'autres fois, j'obtiens une erreur d'exécution indiquant

abort() a été appelé

Alors quelle serait la solution possible pour éviter l'erreur?

0 votes

Vous "rejoignez" les fils créés. Si vous ne le faites pas et que vous quittez simplement un fil, alors il peut rester des fils flottants.

15 votes

Note : en général, il est inapproprié d'appeler exit() à partir d'un gestionnaire de signaux.

1 votes

Je pense que vous pouvez appeler _exit() ou _Exit() cependant.

34voto

Joachim Pileborg Points 121221

La manière habituelle est de définir un drapeau atomique (comme std::atomic) qui est vérifié par tous les threads (y compris le thread principal). S'il est défini, alors les sous-threads sortent, et le thread principal commence à join les sous-threads. Ensuite vous pouvez sortir proprement.

Si vous utilisez std::thread pour les threads, c'est une raison possible des plantages que vous rencontrez. Vous devez join le thread avant que l'objet std::thread ne soit détruit.

6 votes

@Hamed Pratiquement? Pour un simple drapeau bool, alors non, pas vraiment. Théoriquement? Oui, c'est une course aux données. Mais il vaut mieux toujours être prudent, donc vous devriez utiliser par exemple std::atomic.

5 votes

@Hamed Lecture supplémentaire utile: Are sig_atomic_t and std::atomic<> interchangable

26 votes

Je suis enclin à dire qu'un booléen non atomique peut définitivement causer des problèmes lorsqu'il est utilisé comme drapeau de sortie de thread. Le principal problème est que les threads vérifient souvent ce drapeau dans une boucle while(! exitThread() ) { doWork(); }. Si c'est une variable non atomique, elle pourrait être soulevée de la boucle if (exitThread) return; while(true) { doWork(); }.

19voto

Jeremy Friesner Points 16684

D'autres ont mentionné que le gestionnaire de signaux définissait un std::atomic et que tous les autres threads vérifiaient périodiquement cette valeur pour savoir quand sortir.

Cette approche fonctionne bien tant que tous les autres threads se réveillent périodiquement de toute façon, à une fréquence raisonnable.

Cela n'est pas entièrement satisfaisant si l'un ou plusieurs de vos threads sont purement basés sur des événements, cependant -- dans un programme basé sur des événements, les threads ne sont censés se réveiller que s'il y a du travail à faire, ce qui signifie qu'ils pourraient bien être endormis pendant des jours ou des semaines. S'ils sont forcés de se réveiller toutes les (tant de) millisecondes simplement pour interroger un drapeau booléen atomique, cela rend un programme par ailleurs extrêmement efficace en termes de CPU beaucoup moins efficace en termes de CPU, car maintenant chaque thread se réveille à de courts intervalles réguliers, 24h/24, 7j/7, 365 jours par an. Cela peut poser particulièrement problème si vous essayez de préserver la durée de vie de la batterie, car cela peut empêcher le CPU de passer en mode économie d'énergie.

Une approche alternative qui évite le polling serait la suivante :

  1. Au démarrage, votre thread principal crée un tube fd ou une paire de sockets (en appelant pipe() ou socketpair())
  2. Le thread principal (ou éventuellement un autre thread responsable) inclut le socket de réception dans son ensemble select() prêt à lire (ou prend une action similaire pour poll() ou toute fonction d'attente-IO sur laquelle le thread bloque)
  3. Lorsque le gestionnaire de signaux est exécuté, il écrit un octet (n'importe quel octet, peu importe) dans le socket d'envoi.
  4. Cela fera en sorte que l'appel à select() du thread principal retourne immédiatement, avec FD_ISSET(receivingSocket) indiquant vrai à cause de l'octet reçu
  5. À ce stade, le thread principal sait qu'il est temps pour le processus de se terminer, il peut donc commencer à demander à tous ses threads enfants de commencer à se fermer (via le mécanisme le plus pratique ; des booléens atomiques, des tubes ou autre chose)
  6. Après avoir demandé à tous les threads enfants de commencer à se fermer, le thread principal devrait ensuite appeler join() sur chaque thread enfant, garantissant ainsi que tous les threads enfants sont réellement partis avant que main() ne retourne. (Ceci est nécessaire car sinon il y a un risque de condition de concurrence -- par exemple, le code de nettoyage post-main() pourrait parfois libérer une ressource tandis qu'un thread enfant toujours en cours d'exécution l'utilisait encore, ce qui pourrait entraîner un crash)

5 votes

En utilisant std::condition_variable<>, il est plus portable qu'un socket et ne bloque pas autant de ressources système.

1 votes

@AnotherParker si vous avez un fil qui est bloqué dans select(), comment faites-vous pour réveiller ce fil et le faire retourner de select() lorsque une variable de condition a été signalée?

0 votes

select() n'est pas portable. Malheureusement, les bibliothèques de communication asynchrones de Boost n'ont pas encore été intégrées dans le standard C, mais éventuellement... voir par exemple boost.org/doc/libs/1_55_0/doc/html/boost_asio/examples/…

14voto

Yakk Points 31636

La première chose que vous devez accepter est que le threading est difficile.

Un "programme utilisant le threading" est aussi générique qu'un "programme utilisant la mémoire", et votre question est similaire à "comment ne pas corrompre la mémoire dans un programme utilisant la mémoire ?"

La façon de gérer un problème de threading est de limiter la façon dont vous utilisez les threads et le comportement des threads.

Si votre système de threading est un ensemble de petites opérations composées en un réseau de flux de données, avec la garantie implicite que si une opération est trop importante, elle est décomposée en opérations plus petites et/ou effectue des checkpoints avec le système, alors l'arrêt est très différent que si vous avez un thread qui charge une DLL externe qui l'exécute pendant une durée allant de 1 seconde à 10 heures à une durée infinie.

Comme pour la plupart des choses en C++, résoudre votre problème va tourner autour de la propriété, de la maîtrise et (en dernier recours) des astuces.

Comme pour les données en C++, chaque thread devrait être propriété de quelqu'un. Le propriétaire d'un thread devrait avoir un contrôle significatif sur ce thread, et être capable de dire que l'application est en train de s'arrêter. Le mécanisme d'arrêt devrait être robuste et testé, et idéalement connecté à d'autres mécanismes (comme l'arrêt précoce des tâches spéculatives).

Le fait que vous appelez exit(0) est un mauvais signe. Cela implique que votre thread d'exécution principale n'a pas de chemin d'arrêt propre. Commencez par là ; le gestionnaire d'interruption devrait signaler au thread principal que l'arrêt doit commencer, puis votre thread principal devrait s'arrêter en douceur. Tous les cadres de pile devraient se dérouler, les données devraient être nettoyées, etc.

Ensuite, le même type de logique qui permet cet arrêt propre et rapide devrait également être appliqué à votre code threadé.

Quiconque vous dit que c'est aussi simple qu'une variable de condition/un booléen atomique et un polling essaie de vous vendre quelque chose d'inepte. Cela ne fonctionnera que dans des cas simples si vous avez de la chance, et déterminer si cela fonctionne de manière fiable va être assez difficile.

0 votes

Les threads sont difficiles uniquement dans certaines langues..

0 votes

@BoppityBop Principalement dans ceux qui sont Turing complètes. Il existe de nombreux langages qui simulent le threading facilement. Ils créent des applications impossibles à rendre fiables, mais qui fonctionnent généralement, donc on les envoie. Les tentatives de rendre simples et robustes le threading se retrouvent, à ma connaissance, dans des langages allant de Go/Rust à Javascript, dans tous lesquels les gens trouvent le threading difficile. C# et Java sont deux exemples de threading cassé par conception, si c'est ce à quoi vous pensez. Certes, bon nombre de ces langages prennent en charge un arrêt plus facile, mais tuer un processus en C++ est également facile.

3voto

HMD Points 1862

En plus de la réponse de Some programmer dude et en lien avec la discussion dans la section des commentaires, vous devez rendre le drapeau qui contrôle la terminaison de vos threads de type atomic.

Considérez le cas suivant :

bool done = false;
void pending_thread()
{
    while(!done)
    {
        std::this_thread::sleep(std::milliseconds(1));
    }
    // faire quelque chose qui dépend des résultats du thread de travail
}

void worker_thread()
{
    // faire quelque chose pour le thread en attente
    done = true;
}

Ici, le fil de travail peut être votre fil principal également et done est le drapeau de terminaison de votre fil, mais le fil en attente doit faire quelque chose avec les données fournies par le fil de travail avant de se terminer.

Cet exemple présente une condition de course et un comportement indéfini avec, et il est vraiment difficile de trouver quel est le problème réel dans le monde réel.

Maintenant la version corrigée en utilisant std::automic :

std::atomic done(false);
void pending_thread()
{
    while(!done.load())
    {
        std::this_thread::sleep(std::milliseconds(1));
    }
    // faire quelque chose qui dépend des résultats du thread de travail
}

void worker_thread()
{
    // faire quelque chose pour le thread en attente
    done = true;
}

Vous pouvez terminer le fil sans vous soucier de la condition de course ou du comportement indéfini.

2 votes

...sauf que cet exemple utilise le polling, qui est un anti-pattern bien connu. Cela va perturber le planificateur de tâches de votre système d'exploitation et entraîner de mauvaises performances du système et gaspiller de l'énergie.

2 votes

@AnotherParker C'est exact, ce genre de situation devrait être gérée par mutex et condition_variable. mais l'exemple vise simplement à démontrer la situation qui pourrait poser problème sans automic.

0 votes

@AnotherParker Et l'autre chose est que cette approche peut encore être utilisée dans certaines situations, envisagez d'avoir un thread de rendu qui attend qu'un autre thread produise des trames de données à rendre, vous voudrez peut-être afficher d'autres informations ou une animation de chargement tant que les données ne sont pas fournies, je sais que vous pouvez y parvenir en utilisant un délai de variable de condition, mais cela aurait le même effet que celui-ci. Ce n'est pas toujours le cas d'une mauvaise performance du système ou d'une consommation inutile de puissance, parfois vos besoins sont différents.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X