Est-il sécuritaire de partir d’un thread?

Permettez-moi de vous expliquer: j’ai déjà développé une application sur Linux qui forge et exécute un fichier binary externe et attend sa fin. Les résultats sont communiqués par les fichiers shm uniques au processus fork +. Le code entier est encapsulé dans une classe.

J’envisage maintenant d’enfiler le processus pour accélérer les choses. Avoir plusieurs instances de fonctions de classe différentes exécuter et exécuter le binary simultanément (avec des parameters différents) et communiquer des résultats avec leurs propres fichiers shm uniques.

Ce fil est-il sûr? Si je mets un fil dans un fil, en dehors de la sécurité, y a-t-il quelque chose que je dois surveiller? Tout conseil ou aide est très apprécié!

fork , même avec des fils, est sûre. Une fois que vous avez bifurqué, les threads sont indépendants par processus. (C’est-à-dire que le filetage est orthogonal à la bifurcation). Toutefois, si les threads des différents processus utilisent la même mémoire partagée pour la communication, vous devez concevoir un mécanisme de synchronisation.

Le problème est que fork () ne copie que le thread appelant et que tous les mutex contenus dans les threads enfants seront à jamais verrouillés dans l’enfant forked. La solution pthread était les gestionnaires pthread_atfork() . L’idée était que vous pouvez enregistrer 3 gestionnaires: un préfork, un parent et un gestionnaire. Lorsque fork() se produit, prefork est appelé avant fork et devrait obtenir tous les mutex d’application. Les parents et les enfants doivent libérer tous les mutex dans les processus parent et enfant respectivement.

Ce n’est pas la fin de l’histoire cependant! Les bibliothèques appellent pthread_atfork pour enregistrer les gestionnaires de mutex spécifiques à la bibliothèque. Par exemple, Libc le fait. C’est une bonne chose: l’application ne peut probablement pas connaître les mutex détenus par les bibliothèques tierces, donc chaque bibliothèque doit appeler pthread_atfork pour s’assurer que ses propres mutex sont nettoyés en cas de fork() .

Le problème est que l’ordre dans lequel les gestionnaires de pthread_atfork sont appelés pour les bibliothèques non liées est indéfini (cela dépend de l’ordre dans lequel les bibliothèques sont chargées par le programme). Cela signifie donc que, techniquement, une impasse peut se produire à l’intérieur d’un gestionnaire de pré-charge en raison d’une situation de concurrence.

Par exemple, considérons cette séquence:

  1. Thread T1 appelle fork()
  2. les gestionnaires préfork pour libc obtenus en T1
  3. Ensuite, dans Thread T2, une bibliothèque tierce A acquiert son propre mutex AM, puis effectue un appel libc qui nécessite un mutex. Cela bloque, car les mutex libc sont détenus par T1.
  4. Thread T1 exécute le gestionnaire prefork pour la bibliothèque A, qui bloque en attente d’obtenir AM, qui est détenu par T2.

Il y a votre impasse et ses liens avec vos propres mutex ou code.

Cela s’est effectivement passé sur un projet sur lequel j’ai travaillé une fois. Le conseil que j’avais trouvé à l’époque était de choisir une fourchette ou un fil, mais pas les deux. Mais pour certaines applications, ce n’est probablement pas pratique.

Il est prudent de créer un programme multithread tant que vous faites très attention au code entre fork et exec. Vous ne pouvez effectuer que des appels système ré-entrants (asynchrones-safe) au cours de cette période. En théorie, vous n’êtes pas autorisé à y accéder ni à le libérer, bien que, dans la pratique, l’allocateur Linux par défaut soit sûr et que les bibliothèques Linux en soient dépendantes. Le résultat final est l’utilisation de l’allocateur par défaut.

Bien que vous puissiez utiliser le support pthreads(7) NPTL de Linux pour votre programme, les threads sont mal adaptés aux systèmes Unix, comme vous l’avez découvert avec votre question fork(2) .

Étant donné que fork(2) est une opération très peu coûteuse sur les systèmes modernes, vous pouvez faire mieux de simplement fork(2) votre processus lorsque vous avez plus de gestion à effectuer. Cela dépend de la quantité de données que vous avez l’intention de déplacer, la philosophie share-nothing des processus fork eded est utile pour réduire les bogues de données partagées, mais vous devez créer des canaux pour déplacer les données entre les processus ou utiliser la mémoire partagée ( shmget(2) ou shm_open(3) ).

Mais si vous choisissez d’utiliser le threading, vous pouvez créer un nouveau processus fork(2) avec les indications suivantes à partir de la page de manuel fork(2) :

  * The child process is created with a single thread — the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause. 

De retour à l’aube du temps, nous avons appelé les processus «processus légers» car, même s’ils agissent beaucoup comme des processus, ils ne sont pas identiques. La plus grande distinction est que, par définition, les threads vivent dans le même espace d’adresse d’un processus. Cela présente des avantages: le passage d’un thread à un autre est rapide, il partage de manière inhérente de la mémoire, les communications entre threads sont rapides et la création et l’élimination de threads sont rapides.

La distinction est faite avec les “processus lourds”, qui sont des espaces d’adresse complets. Un nouveau processus lourd est créé par fork (2) . Au fur et à mesure de l’arrivée de la mémoire virtuelle dans le monde UNIX, celle-ci a été augmentée avec vfork (2) et d’autres.

Un fork (2) copie l’intégralité de l’espace d’adressage du processus, y compris tous les registres, et place ce processus sous le contrôle du planificateur du système d’exploitation; la prochaine fois que le planificateur arrive, le compteur d’instructions reprend à la prochaine instruction – le processus enfant forké est un clone du parent. (Si vous voulez exécuter un autre programme, par exemple parce que vous écrivez un shell, vous suivez le fork avec un appel exec (2) , qui charge ce nouvel espace d’adresse avec un nouveau programme, remplaçant celui qui a été cloné.)

Fondamentalement, votre réponse est enfouie dans cette explication: lorsque vous avez un processus avec beaucoup de threads LWP et que vous exécutez le processus, vous aurez deux processus indépendants avec de nombreux threads, exécutés simultanément.

Cette astuce est même utile: dans de nombreux programmes, vous avez un processus parent qui peut comporter de nombreux threads, dont certains génèrent de nouveaux processus enfants. (Par exemple, un serveur HTTP peut le faire: chaque connexion au port 80 est gérée par un thread, puis un processus fils pour un programme CGI peut être créé; exec (2) est alors appelé pour exécuter le programme CGI.) à la place du processus parent fermé.)

Pourvu que vous appeliez rapidement exec ou _exit dans le processus enfant forked, vous êtes en règle dans la pratique.

Vous pourriez vouloir utiliser posix_spawn () à la place, ce qui fera probablement la bonne chose.

Si vous utilisez l’appel système unix ‘fork ()’, vous n’utilisez pas techniquement les threads, vous utilisez des processus, ils auront leur propre espace mémoire et ne pourront donc pas interférer les uns avec les autres.

Tant que chaque processus utilise des fichiers différents, il ne devrait y avoir aucun problème.