Skip to content

MOOC NSI - fondamentaux.

Transcription de la vidéo

4.2.3.4 : Le problème de concurrence

[00:00:01]

Maintenant, nous allons nous intéresser aux problèmes de concurrence qui apparaissent lorsque nous avons des activités concurrentes. C'est le cas typiquement des programmes multi-threadés. Pour montrer le problème de concurrence, , nous allons considérer un programme multi-threadé.. Dans ce programme, nous allons avoir plusieurs threads qui s'exécutent en même temps. Vu qu'ils évoluent au sein d'un même processus, ils partagent des données et quand ils manipulent ces données partagées, les threads peuvent lire leurs valeurs ou alors les modifier. Le problème avec les opérations de lecture et d'écriture, c'est qu'il faut faire attention et ne pas les faire en même temps puisqu'on peut corrompre les valeurs des variables. L'exemple que vous connaissez tous, c'est la manipulation d'un fichier partagé où si on ne fait pas attention et on ne le manipule en même temps en lecture et en écriture, on risque de lire des choses qui ne font pas sens. Ici, nous allons considérer un programme Python qui fait des calculs simples. Nous allons manipuler une liste de nombres réels. Nous allons avoir 100 éléments initialisés à zéro. Ces éléments vont être manipulés à l'aide de plusieurs threads. Nous allons avoir des threads du type PlusThread qui vont rajouter un à chaque élément de la liste. Les threads de ce type vont boucler mille fois et donc au total, ils vont rajouter plus 1000 à chaque élément. Nous allons aussi avoir des threads de type MinusThread. Ceux-là, au lieu de faire +1 pour chaque élément de la liste, vont faire -1.

[00:01:51]

Donc, ils vont aussi boucler mille fois et au total, ils vont soustraire 1000 à chaque élément de la liste. Nous allons créer le même nombre de threads PlusThread que de threadsMinusThread, ce qui fait qu'à la fin du programme, nous nous attendons à avoir une liste qui contient toujours des éléments avec des valeurs. 0. Voyons le code Python. Pour définir le comportement des threads de type PlusThread, nous définissons la classe PlusThread avec les deux méthodes init, le constructeur, et run pour définir le comportement du thread. Dans run, on dit bien que nous allons utiliser une liste lx qui est globale, donc partagée, et nous avons la boucle qui va s'exécuter ITERATIONS fois avec ITERATIONS égal à 1000 où pour chaque élément, nous allons rajouter un. Le code de MinusThread est le même avec la différence qu'au lieu de faire +1, le thread va soustraire un à chaque élément de la liste. Le thread initial, lui, il démarre avec un affichage comme quoi il se lance et ensuite il définit la liste qui va être partagée entre les différents threads. Il définit que la taille de la liste est 100, il crée une liste vide et ensuite rajoute des éléments avec des valeurs 0. Il exécute la fonction main, où il dit qu'il va bien utiliser la liste qu'il vient d'initialiser et il a deux boucles pour créer les threads. Il y a une première boucle pour créer des threads de type PlusThread et une deuxième boucle pour créer des threads de type MinusThread.

[00:03:47]

Ici, de manière globale, nous allons avoir NB_THREADS, donc 10 threads et la moitié vont être des PlusThread et l'autre moitié vont être des MinusThread. Dans la première boucle, la première instruction crée un thread PlusThread, récupère son identifiant et le stocke dans la liste threads. La même chose pour la création des MinusThread. Ainsi, après les deux boucles dans la liste threads, nous allons avoir les identifiants de tous les threads qui ont été créés. Ces threads sont lancés à l'aide de la fonction start et ensuite le thread initial attend la terminaison de tous les threads en utilisant la fonction join. Le programme se termine en affichant le fait que c'est la fin et en affichant la valeur de la liste. Regardons une première exécution. Vous voyez bien la liste avec ses éléments. Il y a majoritairement des zéros, mais il y a également des éléments qui ne sont pas des zéros. Il y a des valeurs négatives et des valeurs positives. Si nous regardons une autre exécution, on a le même phénomène et si nous avons même plus de valeurs, d'autres valeurs, pareilles, négatives, positives. Troisième exécution pareil. En observant plusieurs fois, on a du mal à comprendre d'où sortent ces valeurs. Pourquoi ce n'est pas zéro? Pourquoi ça ne porte pas sur les mêmes éléments? Et en gros, on ne comprend pas ce qui se passe. Pour comprendre ce qui se passe, on va regarder comment on s'exécute…

[00:05:43]

une instruction de haut niveau. Donc, ici, dans notre exemple, nous avons des instructions arithmétiques le +1 ou le -1 sur des réels. En réalité, quand on regarde à plus bas niveau au niveau processeur, ce qui se passe, nous nous rendons compte que cette instruction est décomposée en trois instructions de bas niveau. D'abord, il y a la lecture de la valeur de la variable depuis la mémoire, ensuite, il y a le calcul et au final, il faut écrire la nouvelle valeur en mémoire. Donc, au lieu d'avoir une instruction, nous avons 3 instructions de bas niveau. Et le problème est que l'exécution de ces 3 instructions n'est pas atomique, c'est à dire que ces 3 instructions ne s'exécutent pas en bloc, mais l'exécution peut être interrompue entre les différentes instructions. Si nous regardons ce que cela veut dire avec deux threads, un jaune et un bleu, où le jaune doit exécuter l'opération et doit donc exécuter les trois instructions de bas niveau 1, 2, 3 et le bleu qui doit exécuter les 3 instructions de bas niveau bleu 1, 2, 3, vu que l'ordonnancement est non prévisible, il se peut qu'on ait l'exécution suivante. D'abord, le jeune qui fait le 1 jaune, le bleu qui fait le 1 bleu, le jaune qui fait le 2 jaune, le bleu qui fait le 2 bleu, Jjusqu'à la fin. Si la valeur au début était 4, à la fin, on se retrouve avec une valeur 5 alors que nous avions prévu de faire 2 fois plus 1.

[00:07:50]

Donc nous voulions en avoir 6. Ici, nous avons perdu une opération à cause de la non atomicité d'exécution. Si nous revenons à notre exemple, où nous avons non seulement des +1, mais des -1, le phénomène est exactement pareil. Donc si nous avons ici un thread qui est un PlusThread et donc fait +1, il devrait exécuter des instructions de bas niveau 1, 2, 3 qui sont montrées ici. Alors que les threads qui sont de type MinusThread doivent exécuter les instructions qui sont montrées ici.

Si nous avons un PlusThread et quatre MinusThread et si nous avons cet ordonnancement là en commençant avec une valeur 1, on arrive à une valeur -1 alors que nous voulions avoir un plus 1 (=) 2, -4 (=) -2. Donc, de nouveau, l'ordonnancement et la non atomicité a fait que nous avons perdu des opérations.

Nous avons illustré dans cette séquence le problème de concurrence. Nous avons vu que ce problème apparaît quand nous avons des activités en parallèle qui travaillent sur des données partagées en les manipulant avec des opérations conflictuelles. Si vous n'avez pas d'activité parallèle, ou alors vous avez des activités parallèles qui ne travaillent pas sur les mêmes données, ou alors elles travaillent sur les mêmes données, mais uniquement en lecture, alors vous n'avez pas de problème de concurrence. Le problème de concurrence fait que les valeurs des ressources peuvent être corrompues, ce qui peut créer des comportements incohérents.