Alan Cox a dit un jour : "Un ordinateur est une machine d'état. Les threads sont destinés aux personnes qui ne peuvent pas programmer de machines d'état".
Puisque demander à Alan directement n'est pas une option pour moi, je préfère demander ici: comment peut-on obtenir une fonctionnalité multi-threading dans un langage de haut niveau, tel que Java, en utilisant un seul thread et une machine d'état? Par exemple, que faire s'il y a 2 activités à effectuer (faire des calculs et faire des E / S) et qu'une activité peut bloquer?
L'utilisation d'une machine à états uniquement est-elle une alternative viable au multithread dans les langues de haut niveau?
multithreading
finite-state-machine
Victor Sorokin
la source
la source
Réponses:
Un thread ne fait qu'entrelacer des opérations de sorte que certaines parties du processus semblent se chevaucher dans le temps. Une machine monocœur à plusieurs threads se contente de sauter: elle exécute de petits morceaux de code à partir d'un thread, puis passe à un autre thread. Un ordonnanceur simple décide quel thread a la priorité la plus élevée et est réellement exécuté dans le noyau.
Sur un ordinateur monocœur, rien ne se passe réellement "en même temps". C'est juste une exécution entrelacée.
Il existe de très nombreuses façons de réaliser l'entrelacement. Beaucoup.
Supposons que vous ayez un processus simple à deux threads qui utilise un verrou simple afin que les deux threads puissent écrire dans une variable commune. Vous avez six blocs de code.
[Cela peut être dans une boucle ou avoir plus de verrous ou autre. Tout cela ne fait que s'allonger, pas être plus complexe.]
Les étapes de T1 doivent s'exécuter dans l'ordre (T1-avant, T1-avec, T1-après) et les étapes de T2 doivent s'exécuter dans l'ordre (T2-avant, T2-avec, T2-après).
Outre la contrainte "dans l'ordre", celles-ci peuvent être entrelacées de n'importe quelle manière. En tous cas. Ils peuvent être exécutés comme indiqué ci-dessus. Un autre ordre valide est (T1-avant, T2-avant, T2-verrou, T1-verrou, T2-après, T1-après). Il y a beaucoup de commandes valides.
Attendez.
Ce n'est qu'une machine à états avec six états.
Il s'agit d'un automate à états finis non déterministe. L'ordre des états T1-xxx avec les états T2-xxx est indéterminé et n'a pas d'importance. Il y a donc des endroits où le "prochain état" est un tirage au sort.
Par exemple, lorsque le FSM démarre, T1-avant ou T2-avant sont tous deux des premiers états légitimes. Un tirage au sort.
Disons que c'est venu T1-avant. Faites ça. Lorsque cela est fait, vous avez le choix entre T1 avec et T2 avant. Un tirage au sort.
À chaque étape du FSM, il y aura deux choix (deux fils - deux choix) et un tirage au sort peut déterminer quel état spécifique est suivi.
la source
L'écriture de fonctions de blocage est destinée aux personnes qui ne peuvent pas créer de machines d'état;)
Les discussions sont utiles si vous ne pouvez pas contourner le blocage. Aucune activité informatique fondamentale ne bloque vraiment, c'est juste que beaucoup d'entre eux sont mis en œuvre de cette façon pour une facilité d'utilisation. Au lieu de renvoyer un caractère ou "échec de lecture", une fonction de lecture se bloque jusqu'à ce que tout le tampon soit lu. Au lieu de vérifier le message de retour dans une file d'attente et de retourner s'il n'en trouve aucun, une fonction de connexion attend la réponse.
Vous ne pouvez pas utiliser les fonctions de blocage dans une machine d'état (au moins une qui ne peut pas être autorisée à "geler").
Et oui, l'utilisation de la machine d'état est une alternative viable. Dans les systèmes en temps réel, c'est la seule option, le système fournissant un cadre pour la machine. L'utilisation de threads et de fonctions de blocage n'est que "la solution la plus simple", car généralement un appel à une fonction de blocage remplace environ 3-4 états dans la machine d'état.
la source
Ce que vous décrivez s'appelle le multitâche coopératif , où les tâches reçoivent le processeur et devraient y renoncer volontairement après un certain temps ou une activité auto-déterminée. Une tâche qui ne coopère pas en continuant à utiliser le CPU ou en bloquant gomme tout le travail et à moins d'avoir une horloge de surveillance matérielle, il n'y a rien que le code supervisant les tâches puisse faire à ce sujet.
Ce que vous voyez dans les systèmes modernes est appelé multitâche préemptif , c'est-à-dire que les tâches n'ont pas à abandonner le processeur parce que le superviseur le fait pour elles lorsqu'une interruption générée par le matériel arrive. La routine de service d'interruption dans le superviseur enregistre l'état du CPU et le restaure la prochaine fois que la tâche est considérée comme méritant une tranche de temps, puis restaure l'état de la tâche à exécuter ensuite et y revient comme si rien ne s'était passé . Cette action est appelée un changement de contexte et peut être coûteuse.
Viable? Sûr. Sain? Parfois. Que vous utilisiez des threads ou une forme de multitâche coopératif fait maison (par exemple, des machines d'état) dépend des compromis que vous êtes prêt à faire.
Les threads simplifient la conception des tâches au point où vous pouvez traiter chacun comme son propre programme qui partage l'espace de données avec d'autres. Cela vous donne la liberté de vous concentrer sur le travail à accomplir et non sur toute la gestion et l'entretien ménager requis pour le faire fonctionner une itération à la fois. Mais comme aucune bonne action n'est impunie, vous payez pour toute cette commodité dans des changements de contexte. Avoir de nombreux threads qui produisent le CPU après avoir effectué un travail minimal (volontairement ou en faisant quelque chose qui bloquerait, comme les E / S) peut consommer beaucoup de temps processeur pour changer de contexte. Cela est particulièrement vrai si vos opérations de blocage se bloquent rarement très longtemps.
Il y a des situations où la route coopérative a plus de sens. Une fois, j'ai dû écrire un logiciel utilisateur pour un morceau de matériel qui diffusait de nombreux canaux de données via une interface mappée en mémoire qui nécessitait une interrogation. Chaque canal était un objet construit de telle manière que je pouvais le laisser s'exécuter en tant que thread ou exécuter plusieurs fois un cycle d'interrogation.
Les performances de la version multithread n'étaient pas bonnes du tout pour la raison que j'ai décrite ci-dessus: chaque thread effectuait un travail minimal et produisait ensuite le CPU afin que les autres canaux puissent avoir un peu de temps, provoquant de nombreux changements de contexte. Le fait de laisser les threads s'exécuter gratuitement jusqu'à ce qu'ils soient préemptés a contribué au débit, mais certains canaux n'ont pas été réparés avant que le matériel ne subisse un dépassement de tampon car ils n'ont pas obtenu une tranche de temps assez tôt.
La version à un seul fil, qui effectuait même des itérations de chaque canal, fonctionnait comme un singe échaudé et la charge sur le système tombait comme un rocher. La pénalité que j'ai payée pour la performance supplémentaire était d'avoir à jongler avec les tâches moi-même. Dans ce cas, le code pour le faire était suffisamment simple pour que le coût de développement et de maintenance en vaille la peine. Je suppose que c'est vraiment le résultat final. Si mes fils avaient été ceux qui étaient restés assis à attendre le retour d'un appel système, l'exercice n'aurait probablement pas valu la peine.
Cela m'amène au commentaire de Cox: les threads ne sont pas exclusivement réservés aux personnes qui ne peuvent pas écrire de machines d'état. Certaines personnes sont tout à fait capables de le faire, mais choisissent d'utiliser une machine d'état en conserve (c'est-à-dire un thread) dans le but de faire le travail plus tôt ou avec moins de complexité.
la source
Eh bien, honnêtement, je ne peux pas imaginer comment gérer le blocage des E / S sans threads. C'est ce qu'on appelle le blocage après tout simplement parce que le code qui l'invoque doit le faire
wait
.D'après ma lecture de l'e-mail original de Cox (ci-dessous), il souligne que le filetage ne s'adapte pas bien. Je veux dire, que faire s'il y a 100 demandes d'E / S? 1000? 10000? Cox souligne que le fait d'avoir un grand nombre de threads peut entraîner de graves problèmes:
source: Re: Analyse intéressante du threading du noyau linux par IBM (archives de la liste de diffusion linux-kernel)
la source
En théorie, c'est vrai. Dans la vraie vie, les threads ne sont qu'une abstraction efficace utilisée pour programmer une telle machine à états. Ils sont si efficaces qu'ils peuvent également être utilisés pour programmer des diagrammes statiques et des réseaux de Petri (c'est-à-dire des comportements parallèles, où les machines d'état sont essentiellement séquentielles).
Le problème avec les machines à états est l'explosion combinatoire. Le nombre d'états d'un ordinateur avec 4G RAM est de 2 ^ (2 ^ 32) états (sans compter le lecteur de disque 2T).
Pour un homme dont le seul outil est un marteau, chaque problème ressemble à un clou.
la source
Les threads sont la seule option dans deux cas:
La seconde est pourquoi la plupart des gens pensent que les threads sont inévitables pour faire des IO ou de la programmation réseau, mais c'est généralement parce qu'ils ne savent pas que leur système d'exploitation a une API plus avancée (ou ne veulent pas se battre avec son utilisation).
En ce qui concerne la facilité d'utilisation et la lisibilité, il existe toujours des boucles d'événements (comme libev ou EventMachine ) qui rendent la programmation d'une machine d'état presque aussi simple que de le faire avec des threads, tout en donnant suffisamment de contrôle pour oublier les problèmes de synchronisation.
la source
Un bon moyen de comprendre la façon dont les machines d'état et le multithreading interagissent est de regarder les gestionnaires d'événements GUI. De nombreuses applications / frameworks GUI utilisent un seul thread GUI qui interrogera les sources d'entrée possibles et appellera une fonction pour chaque entrée reçue; essentiellement, cela pourrait être écrit comme un énorme commutateur:
Maintenant, il devient clair assez rapidement que le niveau de contrôle de haut niveau dans cette construction ne peut pas être élevé: le gestionnaire de ButtonPressed doit terminer sans interaction avec l'utilisateur et retourner à la boucle principale, car si ce n'est pas le cas, aucun autre événement utilisateur peut être traité. S'il a un état à enregistrer, cet état doit être enregistré dans des variables globales ou statiques, mais pas sur la pile; c'est-à-dire que le flux de contrôle normal dans un langage impératif est limité. Vous êtes essentiellement limité à une machine d'état.
Cela peut devenir assez compliqué lorsque vous avez des sous-programmes imbriqués qui doivent enregistrer, par exemple, un niveau de récursivité. Ou vous êtes en train de lire un fichier, mais le fichier n'est pas disponible pour le moment. Ou sont juste dans un long calcul. Dans tous ces cas, il devient souhaitable de sauvegarder l'état de l'exécution en cours et de revenir à la boucle principale, et c'est du multithreading . Ni plus ni moins.
Le tout est devenu un peu plus compliqué avec l'introduction du multithreading préemptif (c'est-à-dire que le système d'exploitation décide quand les threads doivent céder le contrôle), et c'est pourquoi la connexion n'est pas immédiatement claire aujourd'hui.
Donc, pour répondre à la dernière question: Oui, la machine d'état est une alternative, la plupart des interfaces graphiques fonctionnent de cette façon avec le thread GUI. Ne poussez pas trop loin la machine d'état, elle devient très difficile à maintenir très rapidement.
la source
Demander si l'utilisation d'une machine d'état est viable dans un langage de haut niveau revient un peu à se demander si l'écriture dans l'assembleur est une alternative viable à l'utilisation d'un langage de haut niveau. Ils ont tous les deux leur place, vu la bonne situation.
L'abstraction de l'utilisation du threading rend les systèmes parallèles plus complexes plus faciles à implémenter, mais finalement tous les systèmes parallèles ont les mêmes problèmes à traiter. Les problèmes classiques comme Deadlock / Livelock et l'inversion de priorité sont tout aussi possibles avec des systèmes basés sur des machines à états qu'avec un système parallèle à mémoire partagée , NUMA ou même basé sur CSP , s'il est suffisamment complexe.
la source
Je ne pense pas que ce soit le cas - bien sûr, les machines à états sont un concept informatique très «élégant» mais comme vous le dites, elles sont assez compliquées. Et les choses compliquées sont difficiles à réussir. Et les choses qui ne vont pas sont juste cassées, donc à moins que vous ne soyez un génie de la stature présumée d'Alan Cox, restez avec des choses que vous savez fonctionner - laissez le `` codage intelligent '' aux projets d'apprentissage.
Vous pouvez dire quand quelqu'un a tenté en vain d'en faire un, car (en supposant que cela fonctionne bien) quand il s'agit de le maintenir, vous trouvez que la tâche est presque impossible. Le `` génie '' d'origine aura évolué en vous laissant avec le morceau de code à peine compréhensible (car ce type de développeurs n'a pas tendance à laisser trop de commentaires, sans parler de la documentation technique).
Dans certains cas, une machine d'état sera un meilleur choix - je pense à des trucs de type intégré maintenant où certains modèles de machine d'état sont utilisés, et utilisés de manière répétée et de manière plus formalisée (c'est-à-dire une ingénierie appropriée :))
Le threading peut également être difficile à réaliser correctement, mais il existe des modèles pour vous aider - principalement en réduisant le besoin de partager des données entre les threads.
Le dernier point à ce sujet est que les ordinateurs modernes fonctionnent de toute façon sur de nombreux cœurs, donc une machine d'état ne tirera pas vraiment parti des ressources disponibles. Le filetage peut faire un meilleur travail ici.
la source
Bon exemple d'une utilisation correcte de la machine d'état au lieu des threads: nginx vs apache2. En général, vous pouvez supposer que nginx gère toutes les connexions dans un thread, apache2 crée un thread par connexion.
Mais pour moi, utiliser des machines d'état vs des threads est assez similaire en utilisant asm vs java parfaitement conçu à la main: vous pouvez obtenir des résultats incroyables, mais cela prend beaucoup d'efforts de programmeurs, beaucoup de discipline, rend le projet plus complexe et ne vaut que lorsqu'il est utilisé par beaucoup d'autres programmeurs. Donc, si vous êtes celui qui veut créer un serveur Web rapide - utilisez des machines d'état et des IO asynchrones. Si vous écrivez le projet (pas la bibliothèque à utiliser partout) - utilisez des threads.
la source