Lecture 17 : Concurrence
#### Logiciel dans 6.005
À l’abri des bogues | Facile à comprendre | Prêt à être modifié |
---|---|---|
Corriger aujourd’hui et corriger dans un avenir inconnu. | Communiquer clairement avec les futurs programmeurs, y compris les futurs vous. | Conçu pour s’adapter au changement sans réécriture. |
#### Objectifs + Passage de message & mémoire partagée + Processus & threads + Découpage temporel + Conditions de concurrence # # Simultanéité * Simultanéité * signifie que plusieurs calculs se produisent en même temps. La concurrence est partout dans la programmation moderne, que cela nous plaise ou non: + Plusieurs ordinateurs dans un réseau + Plusieurs applications fonctionnant sur un ordinateur + Plusieurs processeurs dans un ordinateur (aujourd’hui, souvent plusieurs cœurs de processeur sur une seule puce) En fait, la concurrence est essentielle dans la programmation moderne: + Les sites Web doivent gérer plusieurs utilisateurs simultanés.+ Les applications mobiles doivent effectuer une partie de leur traitement sur des serveurs (“dans le cloud”).+ Les interfaces utilisateur graphiques nécessitent presque toujours un travail en arrière-plan qui n’interrompt pas l’utilisateur. Par exemple, Eclipse compile votre code Java pendant que vous le modifiez encore.Pouvoir programmer avec la concurrence sera toujours important à l’avenir. Les vitesses d’horloge du processeur n’augmentent plus. Au lieu de cela, nous obtenons plus de cœurs à chaque nouvelle génération de puces. Donc, à l’avenir, pour qu’un calcul fonctionne plus rapidement, nous devrons diviser un calcul en morceaux simultanés.## Deux modèles de programmation simultanéeil existe deux modèles courants de programmation simultanée : * mémoire partagée* et * passage de messages*.
** Mémoire partagée.** Dans le modèle de mémoire partagée de la concurrence, les modules concurrents interagissent en lisant et en écrivant des objets partagés en mémoire. D’autres exemples du modèle de mémoire partagée: +A et B peuvent être deux processeurs (ou cœurs de processeur) dans le même ordinateur, partageant la même mémoire physique.+ A et B peuvent être deux programmes s’exécutant sur le même ordinateur, partageant un système de fichiers commun avec des fichiers qu’ils peuvent lire et écrire.+ A et B peuvent être deux threads dans le même programme Java (nous expliquerons ce qu’est un thread ci-dessous), partageant les mêmes objets Java.
** Passage de message.** Dans le modèle de transmission de messages, les modules concurrents interagissent en s’envoyant des messages les uns aux autres via un canal de communication. Les modules envoient des messages et les messages entrants à chaque module sont mis en file d’attente pour traitement. Les exemples incluent: +A et B peuvent être deux ordinateurs dans un réseau, communiquant par des connexions réseau.+ A et B peuvent être un navigateur Web et un serveur Web – A ouvre une connexion à B, demande une page Web et B renvoie les données de la page Web à A. + A et B peuvent être un client et un serveur de messagerie instantanée.+ A et B peuvent être deux programmes s’exécutant sur le même ordinateur dont l’entrée et la sortie ont été connectées par un tuyau, comme `ls | grep` tapé dans une invite de commande.## Processus, Threads, découpage temporelles modèles de transmission de messages et de mémoire partagée concernent la façon dont les modules concurrents communiquent. Les modules concurrents eux-mêmes sont de deux types différents: les processus et les threads.**Processus**. Un processus est une instance d’un programme en cours d’exécution qui est * isolée* d’autres processus sur la même machine. En particulier, il a sa propre section privée de la mémoire de la machine.L’abstraction du processus est un * ordinateur virtuel*. Cela donne au programme l’impression qu’il a toute la machine pour lui-même like comme si un nouvel ordinateur avait été créé, avec de la mémoire fraîche, juste pour exécuter ce programme.Tout comme les ordinateurs connectés sur un réseau, les processus ne partagent normalement aucune mémoire entre eux. Un processus ne peut pas du tout accéder à la mémoire ou aux objets d’un autre processus. Le partage de mémoire entre les processus est * possible* sur la plupart des systèmes d’exploitation, mais il nécessite un effort particulier. En revanche, un nouveau processus est automatiquement prêt pour le passage de messages, car il est créé avec des flux de sortie d’entrée & standard, qui sont le ‘ Système.out ` et ‘System.in ‘flux que vous avez utilisés en Java.**Fil**. Un thread est un lieu de contrôle à l’intérieur d’un programme en cours d’exécution. Considérez-le comme un endroit dans le programme en cours d’exécution, plus la pile d’appels de méthode qui ont conduit à cet endroit auquel il sera nécessaire de revenir.Tout comme un processus représente un ordinateur virtuel, l’abstraction de thread représente un * processeur virtuel*. La création d’un nouveau thread simule la création d’un nouveau processeur à l’intérieur de l’ordinateur virtuel représenté par le processus. Ce nouveau processeur virtuel exécute le même programme et partage la même mémoire que les autres threads en cours.Les threads sont automatiquement prêts pour la mémoire partagée, car les threads partagent toute la mémoire dans le processus. Il nécessite un effort particulier pour obtenir une mémoire “thread-locale” privée à un seul thread. Il est également nécessaire de configurer explicitement le passage de messages, en créant et en utilisant des structures de données de file d’attente. Nous parlerons de la façon de le faire dans une lecture ultérieure.
Comment puis-je avoir de nombreux threads simultanés avec seulement un ou deux processeurs sur mon ordinateur? Lorsqu’il y a plus de threads que de processeurs, la concurrence est simulée par ** découpage temporel **, ce qui signifie que le processeur bascule entre les threads. La figure de droite montre comment trois threads T1, T2 et T3 peuvent être découpés dans le temps sur une machine qui ne dispose que de deux processeurs réels. Dans la figure, le temps descend, donc au début, un processeur exécute le thread T1 et l’autre exécute le thread T2, puis le second processeur bascule pour exécuter le thread T3. Le thread T2 s’arrête simplement, jusqu’à sa prochaine tranche de temps sur le même processeur ou un autre processeur.Sur la plupart des systèmes, le découpage temporel se fait de manière imprévisible et non déterministe, ce qui signifie qu’un thread peut être mis en pause ou repris à tout moment.
mitx:c613ec53e92840a4a506f3062c994673 Traite & Threads ## Exemple de mémoire partagée regardez un exemple de système de mémoire partagée. Le but de cet exemple est de montrer que la programmation simultanée est difficile, car elle peut avoir des bugs subtils.
Imaginez qu’une banque dispose de distributeurs de billets qui utilisent un modèle de mémoire partagée, de sorte que tous les distributeurs de billets peuvent lire et écrire les mêmes objets de compte en mémoire.Pour illustrer ce qui peut mal tourner, simplifions la banque à un seul compte, avec un solde en dollars stocké dans la variable `balance`, et deux opérations `dépôt` et `retrait` qui ajoutent ou suppriment simplement un dollar: “java // supposons que tous les distributeurs de billets partagent un seul compte en banqueprivate static int balance = 0; private static void deposit() {balance = balance + 1; } private static void withdraw() {balance = balance-1; }` `Les clients utilisent les distributeurs de billets pour effectuer des transactions comme celle-ci: ` ` javadeposit(); //put un dollar inwithdraw (); // retirez-le ` ` Dans cet exemple simple, chaque transaction n’est qu’un dépôt d’un dollar suivi d’un retrait d’un dollar, il devrait donc laisser le solde du compte inchangé. Tout au long de la journée, chaque distributeur de billets de notre réseau traite une séquence de transactions de dépôt / retrait.” ‘ java // chaque ATM effectue un tas de transactions qui // modifient le solde, mais le laissent inchangé par la suite. vide statique privé cashMachine() { for(int i = 0; i < TRANSACTIONS_PER_MACHINE; ++ i) { deposit(); // mettez un dollar dans withdraw(); // retirez-le }} “‘ Donc, à la fin de la journée, quel que soit le nombre de distributeurs automatiques de billets en cours d’exécution ou le nombre de transactions que nous avons traitées, nous devrions nous attendre à ce que le solde du compte soit toujours égal à 0.Mais si nous exécutons ce code, nous découvrons fréquemment que le solde à la fin de la journée n’est pas *0. Si plus d’un appel `cashMachine()` s’exécute en même temps, par exemple sur des processeurs distincts du même ordinateur, alors `balance’ peut ne pas être nul à la fin de la journée. Pourquoi pas?##Interleaving Voici une chose qui peut arriver. Supposons que deux distributeurs automatiques de billets, A et B, travaillent tous deux sur un dépôt en même temps. Voici comment l’étape deposit() se décompose généralement en instructions de processeur de bas niveau: “‘get balance (balance = 0) add 1 réécrivez le résultat (balance = 1)` ` Lorsque A et B s’exécutent simultanément, ces instructions de bas niveau s’entrelacent les unes avec les autres (certaines peuvent même être simultanées dans un certain sens, mais nous nous inquiétons de l’entrelacement pour l’instant):”‘A obtenir le solde (solde = 0) A ajouter 1 A réécrire le résultat (solde = 1) B obtenir le solde (solde = 1) B ajouter 1 B réécrire le résultat (solde = 2) ` `Cet entrelacement est bien fine nous nous retrouvons avec le solde 2, donc A et B ont réussi à mettre un dollar. Mais que se passe-t-il si l’entrelacement ressemblait à ceci: “A obtenir le solde (solde = 0) B obtenir le solde (solde = 0) A ajouter 1 B ajouter 1 A réécrire le résultat (solde = 1) B réécrire le résultat (solde = 1)` ` Le solde est maintenant de 1 dollar Le dollar de A a été perdu! A et B lisent tous les deux le solde en même temps, calculent des soldes finaux distincts, puis courent pour stocker le nouveau solde which qui ne prend pas en compte le dépôt de l’autre.##Condition de coursionc’est un exemple de ** condition de course**. Une condition de concurrence signifie que l’exactitude du programme (la satisfaction des postconditions et des invariants) dépend du moment relatif des événements dans les calculs simultanés A et B. Lorsque cela se produit, nous disons “A est dans une course avec B.”Certains entrelacs d’événements peuvent être corrects, dans le sens où ils sont cohérents avec ce qu’un processus unique et non concourant produirait, mais d’autres entrelacs produisent de mauvaises réponses – violant des postconditions ou des invariants.## Peaufiner le Code n’aidera pastoutes ces versions du code de compte bancaire présentent la même condition de concurrence: “‘java // version 1dépôt nul statique privé () { solde = solde + 1; } retrait nul statique privé () {solde = solde-1; } “”” java // version 2dépôt nul statique privé () { solde += 1; } retrait nul statique privé () { solde -= 1; } retrait nul statique privé () { solde-= 1;} “”” java // version 3private static void deposit() { ++balance; } private static void withdraw() {balancebalance; } “‘Vous ne pouvez pas dire simplement en regardant le code Java comment le processeur va l’exécuter. Vous ne pouvez pas dire quelles seront les opérations indivisibles – les opérations atomiques -. Ce n’est pas atomique simplement parce que c’est une ligne de Java. Il ne touche pas la balance une seule fois simplement parce que l’identifiant de balance ne se produit qu’une seule fois dans la ligne. Le compilateur Java, et en fait le processeur lui-même, ne prend aucun engagement quant aux opérations de bas niveau qu’il générera à partir de votre code. En fait, un compilateur Java moderne typique produit exactement le même code pour ces trois versions!La leçon clé est que vous ne pouvez pas dire en regardant une expression si elle sera à l’abri des conditions de course.
## Réorganiser C’est encore pire que ça, en fait. La condition de concurrence sur le solde du compte bancaire peut s’expliquer en termes de différents entrelacs d’opérations séquentielles sur différents processeurs. Mais en fait, lorsque vous utilisez plusieurs variables et plusieurs processeurs, vous ne pouvez même pas compter sur les modifications apportées à ces variables apparaissant dans le même ordre.Voici un exemple` “‘javaprivate boolean ready = false; private int answer = 0; // computeAnswer s’exécute dans un threadprivate void computeAnswer() { answer = 42; ready= true; } // useAnswer s’exécute dans un threadprivate void useAnswer() { while(!prêt) { Fil.yield(); } if(answer == 0) lance une nouvelle exception RuntimeException (“la réponse n’était pas prête!`); } “`Nous avons deux méthodes qui sont exécutées dans des threads différents. ‘computeAnswer’ fait un long calcul, pour finalement trouver la réponse 42, qu’il met dans la variable de réponse. Ensuite, il définit la variable `ready` sur true, afin de signaler à la méthode en cours d’exécution dans l’autre thread, `useAnswer’, que la réponse est prête à être utilisée. En regardant le code, ‘answer’ est défini avant que ready ne soit défini, donc une fois que `useAnswer` voit `ready` comme vrai, il semble raisonnable de supposer que la `réponse` sera 42, non? Pas si.Le problème est que les compilateurs et les processeurs modernes font beaucoup de choses pour rendre le code rapide. L’une de ces choses consiste à faire des copies temporaires de variables comme answer et ready dans un stockage plus rapide (registres ou caches sur un processeur), et à travailler avec elles temporairement avant de les stocker à leur emplacement officiel en mémoire. Le storeback peut se produire dans un ordre différent de celui des variables manipulées dans votre code. Voici ce qui pourrait se passer sous les couvertures (mais exprimé en syntaxe Java pour le rendre clair). Le processeur crée effectivement deux variables temporaires` ‘tmpr` et ‘tmpa’, pour manipuler les champs prêts et répondre:” ‘javaprivate void computeAnswer() { tmpr booléen = ready; int tmpa=answer; tmpa = 42; tmpr= true; ready=tmpr; // < what que se passe-t-il si useAnswer() s’entrelace ici? //ready est défini, mais la réponse ne l’est pas. answer = tmpa; }“mitx: 2bf4beb7ffd5437bbbb9c782bb99b54e Conditions de course ## Exemple de passage de message
Maintenant, regardons l’approche de passage de message de notre exemple de compte bancaire.Maintenant, non seulement les modules de distributeurs automatiques de billets, mais les comptes sont aussi des modules. Les modules interagissent en s’envoyant des messages les uns aux autres. Les demandes entrantes sont placées dans une file d’attente pour être traitées une à la fois. L’expéditeur n’arrête pas de travailler en attendant une réponse à sa demande. Il gère plus de demandes de sa propre file d’attente. La réponse à sa demande revient finalement sous la forme d’un autre message.Malheureusement, le passage de messages n’élimine pas la possibilité de conditions de course. Supposons que chaque compte prenne en charge les opérations “get-balance” et “withdraw”, avec les messages correspondants. Deux utilisateurs, aux distributeurs automatiques de billets A et B, tentent tous deux de retirer un dollar du même compte. Ils vérifient d’abord le solde pour s’assurer qu’ils ne retirent jamais plus que ce que contient le compte, car les découverts déclenchent de grosses pénalités bancaires: `’get-balanceif solde > = 1 puis retirer 1“Le problème est à nouveau l’entrelacement, mais cette fois l’entrelacement des * messages* envoyés sur le compte bancaire, plutôt que les * instructions* exécutées par A et B. Si le compte commence avec un dollar dedans, alors quel entrelacement des messages trompera A et B en pensant qu’ils peuvent tous les deux retirer un dollar, ce qui déborde le compte?Une leçon ici est que vous devez choisir soigneusement les opérations d’un modèle de transmission de messages. “retirer-si-des-fonds-suffisants” serait une meilleure opération que de simplement “retirer”.##La concurrence est difficile à tester et à déboguer Si nous ne vous avons pas convaincu que la concurrence est délicate, voici le pire. Il est très difficile de découvrir les conditions de course en utilisant les tests. Et même une fois qu’un test a trouvé un bogue, il peut être très difficile de le localiser dans la partie du programme à l’origine.Les bogues de concurrence présentent une très mauvaise reproductibilité. Il est difficile de les faire se produire deux fois de la même manière. L’entrelacement des instructions ou des messages dépend du moment relatif des événements fortement influencés par l’environnement. Les retards peuvent être causés par d’autres programmes en cours d’exécution, un autre trafic réseau, des décisions de planification du système d’exploitation, des variations de la vitesse d’horloge du processeur, etc. Chaque fois que vous exécutez un programme contenant une condition de concurrence, vous pouvez avoir un comportement différent. Ces types de bugs sont des ** heisenbugs **, qui sont non déterministes et difficiles à reproduire, par opposition à un “bohrbug”, qui apparaît à plusieurs reprises chaque fois que vous le regardez. Presque tous les bogues de la programmation séquentielle sont des bogues de bohr.Un heisenbug peut même disparaître lorsque vous essayez de le regarder avec `println` ou `debugger`! La raison en est que l’impression et le débogage sont tellement plus lents que les autres opérations, souvent 100 à 1000 fois plus lentes, qu’ils modifient considérablement le calendrier des opérations et l’entrelacement. Donc, en insérant une simple déclaration d’impression dans le cashMachine()` “‘javaprivate static void cashMachine() { for(int i = 0; i < TRANSACTIONS_PER_MACHINE; ++ i) { deposit(); // mettez un dollar dans le système de retrait(); // retirez-le.hors.println(balance); // fait disparaître le bug! }}“`…et soudain, l’équilibre est toujours 0, comme souhaité, et le bug semble disparaître. Mais ce n’est que masqué, pas vraiment fixé. Un changement de timing ailleurs dans le programme peut soudainement faire revenir le bug.La concurrence est difficile à obtenir. Une partie du but de cette lecture est de vous faire un peu peur. Au cours des prochaines lectures, nous verrons des moyens fondés sur des principes de concevoir des programmes simultanés afin qu’ils soient à l’abri de ce type de bogues.mitx : 704b9c4db3c6487c9f1549956af8bfc8 Test de la concurrence # # Résumé + Concurrence: plusieurs calculs s’exécutant simultanément + Mémoire partagée & paradigmes de passage de messages + Processus & threads + Processus est comme un ordinateur virtuel; le thread est comme un processeur virtuel + Conditions de course + Lorsque l’exactitude du résultat (postconditions et invariants) dépend du moment relatif des événementsces idées se connectent à nos trois propriétés clés d’un bon logiciel principalement de manière incorrecte. La concurrence est nécessaire, mais cela pose de graves problèmes d’exactitude. Nous travaillerons à résoudre ces problèmes dans les prochaines lectures.+ ** À l’abri des bugs.** Les bogues de concurrence sont parmi les bogues les plus difficiles à trouver et à corriger, et nécessitent une conception minutieuse à éviter.+ * * Facile à comprendre.** Il est très difficile pour les programmeurs de prédire comment le code concurrent peut s’entrelacer avec d’autres codes concurrents. Il est préférable de concevoir de manière à ce que les programmeurs n’aient pas à y penser. + * * Prêt pour le changement.** Pas particulièrement pertinent ici.