[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Je réponds à moi-même.
Il y a trois solutions pour résoudre le problème.
La première solution consiste à créer deux classes process différentes, selon qu'on souhaite terminer le processus fils ou non. C'est à mon sens une mauvaise chose, parce que le choix de la classe à utiliser sera problématique.
La seconde solution consiste à ajouter un argument au constructeur de std::process() - un énumération qui aurait des valeurs du style terminate_parent, terminate_child ou terminate_none (celui-là, je ne vois pas trop comment l'utiliser...). Si on préfère ajouter un constructeur plutôt que de modifier le constructeur supplémentaire, std::process(F,Args...) est équivalent à std::process(terminate_child, F, Args...). Avec la délégation de constructeur, on devrait obtenir un code final assez propre.
Enfin, la troisième solution : ajouter une fonction this_process::exit(), qui informe le process qu'on doit appeler exit() lorsque le callable a terminé son exécution. Il faut en outre prévoir un mécanisme qui va appeler std::exit() si on est pas en train d'exécuter un callable dans le constructeur de std::process. C'est possible, mais je ne trouve pas ça très propre... Une implémentation pourrait être (c'est du pseudo-code sale, pas du vrai code bien léché ; le but est de montrer le principe):
On peut imaginer passer par un atomic<T*> si nécessaire, mais je n'en vois pas l'utilité.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 // variable globale std::process* __current_process = NULL; // dans le constructeur : process::process(F, Args...) { fork(); if (in_child()) { // puisqu'on a forké, le parent ne voit pas la mise à jour // de la variable globale (elle est privée au processus fils) __current_process = this; // on force le callable à s'exécuter dans un scope // particulier call_callable(F, Args...); F(Args...); if (this->__must_quit) std::exit(this->__exit_code); } } void process::call_callable(F, Args...) { // étant dans un scope particulier, on est sûr que // les objets en automatic storage vont être détruit // à la sortie du scope. F(Args...); } void std::this_process::exit(int exit_code) { if (__current_process) { // cas où on est dans un process __current_process->__exit_code = exit_code; __current_process->__must_exit = true; } else { std::exit(exit_code); } }
Le problème avec cette méthode, c'est que, tout à coup, appeler this_process::exit() n'a pas du tout l'effet escompté si on l'appelle depuis un thread qui n'est pas un thread principal d'un processus fils créé avec std::process. On s'attendrait à quitter le processus, mais non.
Le problème peut être réglé en choisissant un meilleur nom pour la fonction.
Au niveau design, j'avoue ne pas être vraiment tranquille lorsque j'écrit une fonction qui modifie un état global sans que ça ait d'effet visible immédiat. De plus, le comportement de la fonction change selon qu'elle est appelée dans le constructeur de std::process ou autre part, et ça, ça le chagrine aussi (note: même si on enlève l'appel à exit() : this_process est censé contenir des fonctions qui sont valables quel que soit le processus considéré, et pas uniquement un processus créé avec std::process).
Si vous voyez une solution que je n'ai pas vu, je suis preneur - dans le cas contraire, je penche pour la seconde solution (un paramètre supplémentaire sur un second constructeur).
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Je ne suis pas sur que ces solutions répondent bien au besoin.
Dans l'idéal, il faudrait que ces 3 lignes donnent le même résultat :Du coup, peut être faire quelques chose du genre pour le ctor de process
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 std::process p0([=](){ std::exit(0); }); std::process p1([=](){ return 0 }); std::process p2([=](){ }); // return 0 par défaut
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 private: template <class T, class C> struct _exec { static void foo(C c) { c(); exit(EXIT_SUCCESS); } }; template <class C> struct _exec<int, C> { static void foo(C c) { exit(c()); } }; public: template <typename _F, typename... _Args> explicit process(_F&& __f, _Args&&... __args) { auto __callable = bind(forward<_F>(__f), forward<_Args>(__args)...); native_handle_type __h = __bits::__process_impl<__itag>::fork(); if (__h == 0) { _exec<decltype(__callable()), decltype(__callable)>::foo(__callable); } else if (__h < 0) { __bits::__throw(errc::resource_unavailable_try_again, "failed to fork() the process"); } else { _M_id = process::id(__h); } }Car en faisant un std::exit, on est courant que les destructeurs ne seront pas appelés, du coup pas besoin d'essayer de passer outre ce fonctionnement. (enfin je pense)
edit: en relisant je dis des conneries, il faut bien appeler le destructeur des mutex (et autres objets de synchronisation pour les libérer si besoin)
Utiliser atexit() pour inscrire le destructeur (ou une fonction qui se charge de libérer les mutex) à la création d'un mutex (donc dans le constructeur) pourrait aussi être possible![]()
Sur les trois solutions proposées, la solution 2 me plait bien![]()
Dans le principe, oui. Dans mon dernier code, pushé il y a quelques minutes, c'est le cas - modulo le fait que je fais systématiquement un exit(0), même si le callable fait un return 1.
Par contre, je peux aussi écrire :
Je peux vouloir tuer le parent sans tuer le fils (par exemple pour faire un daemon).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 std::process p0([](){}); // le child fait exit(0) std::process p1([](){ return 1; }); // le child fait exit(0), il devrait faire exit(1) ? std::process p2(terminate_flag::terminate_child, [](){}); // le child fait exit(0) std::process p3(terminate_flag::terminate_parent, [](){}); // le parent fait exit(0) std::process p4(terminate_flag::terminate_both, [](){}); // le parent et le child font exit(0) std::process p4(terminate_flag::terminate_none, [](){}); // tout le monde continue
(note: le nom terminate_flag ne me plait guère, mais ça ira pour l'instant).
C'est intéressant. Ceci dit, ça n'est pas un miroir de ce qui est fait dans std::thread, on la valeur de retour du callable est purement et simplement ignorée. Dans la mesure du possible, je préférerais éviter de trop grandes différences.
Le truc bien, c'est que je peux quand même intégrer cette possibilité; au cas où.
Dans mon cas : les std::named_mutex ont besoin d'appeler shm_unlink() dans le destructeur, de manière à prévenir le système que le mutex nommé doit être détruit. Je n'ai pas encore écrit le code pour Windows, donc je ne sais pas ce que ça peut donner, mais il n'est pas impossible qu'on ait un problème similaire (à moins que l'OS ne garde un refcount sur le mutex nommé).
atexit() doit être réservé à l'utilisateur, car c'est lui qui contrôle ce qui se passe. S'il a une fonction enregistrée qui génère une exception, std::terminate() est appelée et les autres fonctions enregistrées peuvent ne pas être appelée (on se retrouve alors dans le cas initial).
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
J'ai implémenté cette fonctionnalité (présente dans la révision 63aa0c489ea104ac0f1019af8fdadb1e49d1d477 de la branche master sur https://code.google.com/p/edt-proces...source/browse/).
J'ai joué un peu avec, et je suis partagé.
D'un coté, obtenir le code d'erreur en sortie d'un programme est intéressant. Ca permet de valider que le programme s'est exécuté correctement - dans la plupart des cas.
D'un autre coté, ça impose d'écrire quelque chose comme
alors que cette écriture ne se fait pas avec std::thread. De plus, certains programmes n'utilisent pas le code de sortie de manière correcte (notamment sous Windows, et aussi un peu sous Unix), ce qui rends le traitement un peu hasardeux (dans le cas où on fait un this_process::exec).
Code : Sélectionner tout - Visualiser dans une fenêtre à part int r = p.join();
Du coup, trois solutions là aussi :
1) on élimine la notion de code de retour - il est ignoré, comme le code de retour des threads.
2) process::join() retourne le code de sortie du process qui s'est exécuté.
3) on ajoute une fonction supplémentaire int process::exit_code() const noexcep, qui renvoie le code de retour après un appel à join() (la fonction throw si le process est joinable() ? le problème se pose maintenant avec detach() et le constructeur par défaut de process).
J'avoue que je n'ai pas de bonne solution ici. Je suis très, très tenté par (1), histoire de préserver une interface très similaire à std::thread.
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Est-ce que tu consideres le code actuel utilisable en production?
EDIT 1> Ah non, certaines features utilisees ne sont pas accessible a VS sans le CTP...
EDIT 2>
Je ne suis pas sur de comprendre ce comportement:
Le terminate, c'est celui du parent, pas de l'enfant, si?~process()
{
if (joinable())
terminate();
}
Les fonctionnalités C++11 suivantes sont utilisées
* template variadique
* decltype
* enum struct
* auto
* délégation de constructeur
* std::move, std::forward
* rrvalue reference
* exception system_error
Je crois que c'est à peu près tout. Est-ce que le CTP est capable de prendre en charge tout ça ?
Effectivement.
Le principe est le même que pour les thread : si on détruit un thread non détaché ou non "joiné", alors on se prends std::terminate() dans le thread qui exécute le destructeur. Les raisons pour ce comportement sur thread sont duales :
* si on fait un join() dans le destructeur, alors on se prends un problème de performance non contrôlable par l'utilisateur.
* si on fait le detach(), alors on se prend un problème de comportement étrange (possiblement un bug)
Vu que le problème est indécidable, on force soit un detach(), soit un join() explicite en exécutant std::terminate() si le thread ou le process est joinable() au moment où le destructeur de l'objet s'exécute.
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Je ne suis pas sur. De toutes facons le CTP n'est pas utilisable en production:
- il est bugge;
- officiellement, il n'y a pas de support puisque c'est une preview;
- la STL n'utilise pas ses features;
Du coup je ne peu pas l'utiliser. Je pense que c'est pareil pour n'importe qui qui veut sortir son produit cette annee.
Oui je comprends bien la similarite avec le std::thread, et cote parent c'est pas mal, mais si j'ai bien suivi, le processus enfant n'est pas detruit avec le parent quand celui ci se termine, si?Le principe est le même que pour les thread : si on détruit un thread non détaché ou non "joiné", alors on se prends std::terminate() dans le thread qui exécute le destructeur. Les raisons pour ce comportement sur thread sont duales :
* si on fait un join() dans le destructeur, alors on se prends un problème de performance non contrôlable par l'utilisateur.
* si on fait le detach(), alors on se prend un problème de comportement étrange (possiblement un bug)
Vu que le problème est indécidable, on force soit un detach(), soit un join() explicite en exécutant std::terminate() si le thread ou le process est joinable() au moment où le destructeur de l'objet s'exécute.
C'est different des threads qui sont embarques tous ensemble dans le processus.
Hors cela ferais une sorte de leak de processus. Tandis que l'interet ici serait d'etre sur que les processus enfants sont toujours dependants du processus parent.
Comme je ne sais pas bien comment marche fork, peut etre que les details m'echappent, mais pour this_process::exec() tout de suite je me dis que ca serait bien que le processus cree ait une vie gere par sont parent.
Aie.
C'est le but. Il est tout à fait possible d'écrire
Ce qui crée un daemon (très minimaliste...).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 std::process p(std::terminate_flag::terminate_parent, [](){ setup_daemon(); while (1) { wait_and_process_request(); } });
fork() a deux buts :
1) la création de process fils afin d'exécuter une tâche particulière dont le parent à besoin. Ceux là sont terminés (on attends la fin par un appel système waitpid()). S'ils ne se terminent pas avant la fin du process parent, c'est un bug.
2) la création de process fils détachés, dont le fonctionnement est autonome, et qui ne sont donc pas contrôlés par le parent. Ils peuvent continuer à fonctionner même si le parent meure. Il y a deux sous-groupes :
2.1) les daemon - ce sont des processus qui n'ont pas accès à la console (similaires aux services sous Windows). On communique généralement avec eux via des IPC (souvent des sockets). On s'en sert pour implémenter des serveurs (web, ftp...) ou des sous-système de contrôle (udevd sous Linux).
2.2) des processus normaux, qui ont leur tâche à faire, et dont le parent se moque royalement. Un processus lancé par crond (lancement de tâches à heure fixe) est libre de faire ce qu'il veut, y compris rester en arrière plan jusqu'à l'extinction de la machine.
Du coup, on ne peut pas vraiment parler de leak de processus dans le cadre d'un fonctionnement normal : leur domaine de fonctionnement est différent des threads.
Ceci dit, ça ne veut pas dire que les leaks de processus n'existent pas : la possibilité de est même bien réelle réelle. Il s'agit en fait de processus zombie (ils sont créés par un parent, mais ce parent n'attends pas leur fin ; du coup, ils restent dans le système). Les zombies (ainsi appelés parce qu'on ne peut pas les tuer, vu qu'ils sont déjà morts) peuvent poser de multiples problèmes sur un système, et notamment empêcher la création de nouveaux processus s'il y a trop de zombies.
Dans les systèmes POSIX, on peut mitiger ce problème en jouant avec les signaux (le processus parent reçoit un signal SIGCHLD lorsqu'un de ses enfants meure). Ce n'est pas génial dans le cadre de la librairie standard, parce que ça veut dire que la lib standard doit mettre en place une callback pour traiter ce signal. Hors signaux et threads ne se mixent pas très très bien (merci POSIX).
A noter que ce comportement provient d'une erreur de programmation. Du coup, je pense que c'est à l'utilisateur de mettre en place le système qui permet de contourner ce problème (je n'ai pas encore trouvé une solution architecturale propre à intégrer dans le proposal, mais ça vient).
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Après ces explications, la solution d'ajouter un paramètre (enum) au constructeur prend son sens, j'avais du mal à voir l'utilité.
Je reviens la dessus. Actuellement il est possible de faireC'est une écriture plutôt lourde pour le simple lancement d'un processus "externe" (dont le code n'est pas dans l'exe).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 std::process p([]() { std::this_process::exec("ls"); });
Serait plus léger et permettrait des optimisations (vfork + execve sous unix, j'ai pas l'équivalent sous la main pour Windows, mais ça se se fait en un appel système).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 std::process p = std::process::exec("ls"); // ou std::process p("ls");
Est-ce envisageable ? Ou ça s'éloigne trop de std::thread ?
Sous Windows, on utiliserait directement CreateProcess() (au lieu d'une pseudo-fonction fork() qui, elle, utiliserait ZwCreateProcess()). C'est un chemin optimisé.
Sur la seconde version : conceptuellement, exec() est une fonction - je dois pouvoir l'utiliser sans utiliser std::process. Par exemple pour écrire ce code, dans un binaire qui se compile en /sbin/preinit :
La fonction est donc, selon moi, nécessaire - car elle est utile par elle-même.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 int main() { ... // on continue sur l'init Système V std::this_process::exec("/sbin/init"); }
Un constructeur std::process(std::string, Args...) est selon moi confus : il n'est pas explicite sur ce qu'il fait. Il ne dit pas, entre autre, si le programme s'exécute dans un processus fils ou dans le processus courant. Le fait d'avoir la dualité exec/constructeur additionnel de std::process() n'est pas vraiment pour
Quand à la première notation, elle suppose que std::exec renvoie un std::process - la fonction fait donc deux choses (création d'un process + exécution d'un binaire). Elle ne permet plus d'écrire le code que je propose, et de plus elle est une source de problème non négligeable : est-ce que la fonction doit faire le join() (et donc être bloquante) ? Ou le detach() ? Ou rien, mais on doit nécessairement récupérer la valeur de retour pour faire le detach ou le join ? (un cas que la librairie standard n'implémente pas : une valeur de retour non récupérée n'est jamais fatale dans la librairie standard).
Ca me semble plus compliqué et plus sensible que la notion actuelle, certes un peu plus lourde (mais quand même pas trop) et qui a le mérite d'être complètement explicite.
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Je ne proposais pas de remplacer std::this_process::exec, mais d'ajouter une façon d'exécuter directement un exe sur un processus fils. (pour éviter les fork sur Windows quand c'est possible)
exec = exec au sens POSIX
exec = std::this_process::exec
Je vois pas comment, si exec appelle CreateProcess, il y a création d'un 2eme fils (sur lequel on à aucun contrôle car on ne récupère pas le std::process correspondant)
Si on sait qu'il n'y a pas de code avant exec alors on peut faire un CreateProcess, sinon fork + exec obligatoire.
Dans ce cas là, il est impossible de savoir si il y a du code avant le execIl est donc impossible de faire ça autrement que par un fork + exec.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 std::process p([]() { std::this_process::exec("ls"); });![]()
Si on utilise exec() dans deux sens différents peut-être une meilleure idée serait-elle d'utiliser un autre nom (.Net utilise Process:Start(), qui retourne un Process).
SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.
"Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
Apparently everyone. -- Raymond Chen.
Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.
C'est justement le fait de retourner un process qui pose problème : si une fonction start() fait ça, alors elle introduit un drôle de précédent dans la librairie standard, à savoir que cette valeur de retour, si elle n'est pas récupérée, va provoquer un crash de l'application qui l'utilise. Il n'y a aucun autre exemple dans la librairie standard d'un tel comportement : toute valeur de retour d'une fonction ou d'une méthode peut être ignorée.
Après, peut être qu'une fonction start_wait et/ou une fonction start_detach pourrait avoir un intérêt (et permettrait au vendeur d'implémenter un fastpath pour ces cas particuliers).
Je vais rajouter ces fonctions (peut-être nommées différemment) dans le proposal.
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Ah, c'est à cause des histoires de zombitude d'*n*x, c'est ça? Le fait qu'un processus père doive systèmatiquement acquitter de la fin d'un fils?cette valeur de retour, si elle n'est pas récupérée, va provoquer un crash de l'application qui l'utilise.
SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.
"Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
Apparently everyone. -- Raymond Chen.
Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.
Pas vraiment. En fait, l'argument est semblable à celui qui a présidé au design de std::thread et de son destructeur : si le process est joinable au moment où le destructeur de l'objet process s'exécute, on a deux possibilités :
* soit le destructeur fait un join() - ce qui est bloquant et peu potentiellement ne jamais terminer
* soit le destructeur fait un detach(), et la tâche continue sans qu'on ait de contrôle dessus - ce qui peut (potentiellement) introduire un bug dans le sens où la tâche fille peut communiquer avec nous (via un système IPC quelconque ; on se retrouverais dans ce cas à continuer à discuter avec une tâche qui, de notre point de vue, n'est plus là).
De plus, quel que soit la solution choisie, il existe une possibilité pour que cette solution ne soit pas ce que l'utilisateur souhaite faire dans tous les cas.
Le comité de normalisation a résolu ce dilemme de la manière la plus simple possible pour std::thread : il est interdit de détruire un thread joinable() - on se prends un std::terminate() dans la face.
Vu qu'on a exactement le même dilemme, il parait intéressant d'utiliser la même solution - c'est ce que j'ai fait.
Du coup, si une fonction foobar() renvoie un std::process, alors il est nécessaire de récupérer la valeur de retour pour faire soit un join(), soit un detach() - sans quoi on se prendra un std::terminate(). Il faut donc écrire :
ou
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 std::process p = foobar(...); p.join();
Dans tous les cas, le code de retour de foobar ne peut pas être ignoré.
Code : Sélectionner tout - Visualiser dans une fenêtre à part foobar(...).detach();
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
J'ai ajouté (et défini) les fonctions std::this_process::spawn() et std::this_process::spawn_wait() dans mon proposal et dans le code. Le code lui-même n'est pas optimal (je continue d'utiliser fork() au lieu de vfork() ; mais le code sous Windows peut directement tirer partie de CreateProcess()).
J'avais annoncé il y a peu avoir ajouté les named_mutex (l'ai-je fait ? je n'en suis même plus sûr). Quoi qu'il en soit, je les ait implémenté.
Sous Linux, les named_mutex sont implémenté vie le système standard de gestion de mémoire partagée et grâce à l'appel système futex(2). L'implémentation a été testée, elle semble fonctionner correctement (mais je ne suis pas à l'abris d'une boulette). Elle utilise aussi les intrinsics d'accès atomique aux variables proposées par gcc (__sync_fetch_and_or(), __sync_val_compare_and_swap()). Son point faible : la nécessité d'utiliser des fonctions qui doivent être ajoutée à la libstdc++ (pour l'implémentation test, je crée une librairie statique à part).
Les named_mutex sont des mutex. Ils peuvent donc être utilisés comme la classe std::mutex existante. La seule différence (de taille) est que les named_mutex nécessitent un nom - de fait, la classe n'a pas de constructeur par défaut - ce qui n'est pas catastrophique en soi.
La suite du développement (et de la définition du proposal) va se faire sur les autres classes de mutex nommés - named_recursive_mutex, named_timed_mutex et named_recursive_timed_mutex. Ensuite viendra la classe named_semaphore - gestion des sémaphores nommés (dont l'implémentation reste très simple). J'en profiterais peut-être pour faire une passe sur une classe semaphore (un proposal indépendant, à priori).
La gestion de la mémoire partagée va poser problème. Dans l'idée, la mémoire partagée est juste une zone de mémoire comme les autres, mais c'est une très mauvaise idée d'y stocker des pointeurs - pour la simple et bonne raison qu'elle n'est pas nécessairement mappée sur la même adresse virtuelle dans les processus l'utilisant. Ca pose un soucis de conversion ptr <--> offset, conversion qui dans l'idéal doit être transparente. Par exemple, j'aprécierais un objet shm_ptr<> qui permet de déréférencer la valeur, mais on ne peut pas stocker cet objet dans la zone de mémoire partagée elle même. L'objet est (selon moi) nécessairement paramétré via un objet "shm" (à définir), ce qui lui permet avec un offset + l'adresse de base de la zone de mémoire partagée de créer un pointeur. En gros, on aurait (en pseudo-C++ bien écrit, vu que c'est très mal écrit dans cet exemple...):
Mais du coup, on ne peut pas utiliser cet objet dans un autre objet qui sera stocké en shm :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 template <class T1, class T2> offptr compute_offset(T1 *from, T2 *to) { return reinterpret_cast<char*>(to) - reinterpret_cast<char*>(from); } template <class T> class shm_ptr { private: some_shm_object *m_shm; ptroff_t m_offset; public: shm_ptr(some_shm_object *shm, T* p) : m_shm(shm), m_offset(compute_offset(shm->base(), p) { } const T* operator*() const { return reintrepret_cast<const T*>(m_shm->base() + m_offset); } T* operator*() { return reintrepret_cast<T*>(m_shm->base() + m_offset); } };
La solution consisterait à ne pas proposer une telle classe, et (sans les interdire) à prévenir que stocker un pointeur lié à un process dans une shm et l'utiliser dans un autre process est un undefined behavior.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 // impossible de faire un vector<my_object,shm_allocator<my_object>> // parce que l'objet contient un shm_ptr<> qui fait référence à la shm utilisée // (différente pour chaque process) class my_object { private: shm_ptr<object> object_ptr; int k; float q; public: ... };
Tout ça pour dire que les zones de mémoire partagée, ce n'est pas forcément aisé à designer. Si vous avez une idée, n'hésitez pas à m'en faire part. Comme vous avez vu depuis l'ouverture de ce fil de discussion, je prends volontiers vos idées en compte.
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
J'ai été ennuyé par la gestion de la mémoire partagée (et pas que ; je suis toujours en train de chercher comment implémenter des mutex récursifs ; mais c'est une autre histoire), mais je crois que finalement, je tiens le bon bout.
La mémoire partagée a ceci de particulier qu'elle peut être mappée par plusieurs process à des adresses différentes. Du coup, elle ne peut pas stocker de pointeur, parce qu'un pointeur valide pour un process sera probablement invalide pour d'autres (linux permet de dire à l'OS à quelle adresse il faut mapper la zone mémoire ; c'est dangereux). Alors que je réfléchissait à un moyen de mitiger le problème, j'ai eu un éclair (non, pas de génie. Un vrai éclair).
Prenons le cas d'un programme qui veut stocker des données dans la mémoire partagée. Ce programme en a besoin pour communiquer avec un autre programme. De quelle manière va-t-il se servir de cette zone de mémoire partagée ? Comment va-t-il implémenter la lecture et l'écriture ?
Deux types d'accès sont possibles :
* accès aléatoires : il va adresser directement une zone particulière de la mémoire partagée, de manière à y lire ou y écrire directement.
* accès séquentiel : il va utiliser la mémoire partagée comme un flux.
Les accès aléatoires sont problématique car ils encouragent le stockage direct d'informations - mais on peut trouver des pointeurs parmi ces informations. Du coup, avoir une solution qui rends ce type d'accès plus complexe devrait permettre de limiter les problèmes.
Dans ma première vision, j'avais pensé à un allocateur qui serait utilisé pour stocker des collections d'objets (par exemple un std::vector<> ou un std::list<>). Mais ce faisant, on cache si bien les particularités de la mémoire partagée qu'on finit par autoriser des choses horribles. Sans compter qu'un tel allocateur devra nécessairement prendre en compte des problèmes comme la fragmentation de la mémoire ou la réutilisation des blocs libérés. Des algorithmes qui, s'ils ne sont pas trop complexes (OK ; la défragmentation générique avec heuristique de découverte des pointeurs commence à être un peu plus complexe), restent coûteux en terme de temps CPU. Cette prise en compte est nécessaire, dans le sens où un allocateur dans la librairie standard doit pouvoir être utilisé avec tous les types de la librairie standard. J'ai donc abandonné cette idée - elle me semble trop hasardeuse.
Ma seconde vision est plus simple : une classe shared_object<T> permet de stocker un seul objet de type T dans une zone de mémoire partagée. Le nommage est certainement à revoir, parce que ça fait trop penser à shared_ptr<> sans pour autant proposer la même sémantique (conceptuellement, c'est un objet encapsulé, pas un pointeur intelligent sur un objet, encore que l'interface soit au final assez similaire). On reste avec le même type de problème : il est aisé de stocker des choses qui ne doivent pas y être stockée (c'est à dire des pointeurs). Il y a un second problème, presque rédhibitoire.
Si un programme crée un grand nombre de ces objets, alors une page mémoire sera allouée à chaque objet, même si cet objet est de petite taille. Hors les pages mémoires sont une quantité limitées (pas en nombre, mais parce que chaque page est associée à une entrée dans la MMU ; plus le nombre d'entrées est important, plus le système et le processeurs doivent travailler pour les gérer). Du coup, alors que je veux utiliser 1000x20o octets de mémoire, j'en utilise en fait 1000x4096 (en 32 bits ; les pages mémoire en 64 bits sont souvent plus grosses). Et en plus, je ralenti l'OS. Cette solution, si elle semble à peu prêt intéressante sur le papier, n'est pas une bonne idée dans la pratique. J'abandonne donc aussi.
Reste une troisième solution, qui a l'avantage de se baser sur un mécanisme déjà connu et largement utilisé en C++ : une classe de stream.
Une zone de mémoire partagée est crée de la même manière qu'un fichier sur la plupart des OS (au moins sous Windows et sur les OS POSIX, ce qui représente déjà une grosse partie du marché). Il fait donc sens de la représenter aussi sous la forme d'un fichier au niveau C++. Un tel fichier propose une interface permettant un accès aléatoire (seek, read, write), et c'est aussi un flux (donc operator<< + operator>>). On a donc là quelque chose qui correspond conceptuellement à ce qu'est la mémoire partagée vu de l'OS et qui propose en outre les outils dont on a besoin pour accéder à cette mémoire partagée. De plus, l'accès par flux décourage très nettement l'utilisation de pointeurs (sérialisation), ce qui est un bon point. Pour ceux qui veulent faire des choses étranges, les méthodes read/write restent disponibles, même si la sémantique de ces fonctions est légèrement différente de celle d'un fichier (vu qu'une zone de mémoire partagée est créée avec une taille fixe).
Voilà. Qu'en pensez-vous ? Est-ce que vous avez d'autres idées, ou des arguments qui pourrait faire que mon idée n'est pas viable ?
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Ta seconde solution me fait un peu penser aux interior_ptr<> de .Net, malgré des différences.J'ai rien dit, j'ai mal lu.
Ta troisième solution, n'est-ce pas ainsi que marche la mémoire partagée POSIX?
SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.
"Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
Apparently everyone. -- Raymond Chen.
Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.
[FAQ des forums][FAQ Développement 2D, 3D et Jeux][Si vous ne savez pas ou vous en êtes...]
Essayez d'écrire clairement (c'est à dire avec des mots français complets). SMS est votre ennemi.
Evitez les arguments inutiles - DirectMachin vs. OpenTruc ou G++ vs. Café. C'est dépassé tout ça.
Et si vous êtes sages, vous aurez peut être vous aussi la chance de passer à la télé. Ou pas.
Ce site contient un forum d'entraide gratuit. Il ne s'use que si l'on ne s'en sert pas.
Partager