Skip to content

MOOC NSI - fondamentaux.

Transcription de la vidéo

4.2.3.5 : Synchronisation

[00:00:01]

Pour résoudre le problème de concurrence, nous allons avoir besoin de synchronisation. Nous avons vu dans la séquence précédente que lorsque nous avons des activités en parallèle qui travaillent sur des données partagées avec des opérations conflictuelles, donc des lectures et des écritures, nous avons des problèmes d'incohérence. Ici, on revoit l'exemple où nous avons des threads qui exécutent plusieurs opérations et où l'ordonnancement fait que la valeur à la fin n'est pas celle que nous voulons. Cela venait du fait que nos opérations étaient non atomiques. Pour garantir donc la cohérence des données partagées, nous avons besoin d'atomicité, nous avons besoin de pouvoir garantir que certaines opérations ne peuvent pas être interrompues en milieu. Donc, si nous avons une opération, qui est une suite d'opérations de plus bas niveau, nous avons besoin que cette suite s'exécute de manière insécable. Donc, soit on exécute toutes les opérations, soit on n’en exécute aucune. Dans le cas des threads, pour garantir donc la cohérence, on a besoin que quand ils s'exécutent, ils ne se gênent pas. Donc l'exécution dans notre exemple des threads qui faisaient des additions, les PlusThread, ne doivent pas perturber l'exécution d'autres threads qui font des additions ou d'autres threads qui font des soustractions. Or, vu que les threads, on l'a vu, évoluent dans le même espace qui est celui du processus, on avait dit qu'ils n'étaient pas isolés. D'où notre problème de concurrence. Donc, pour pouvoir les isoler les uns des autres et garantir une manipulation correcte des ressources ou des données partagées, nous avons besoin d'autres mécanismes.

[00:02:05]

Donc, il y a la notion de section critique dans le code des applications où, justement, les activités parallèles vont manipuler des données partagées. Et donc, ça va être des sections de code sensibles où il faudrait exercer du contrôle sur l'accès à cette section et sur l'ordre d'exécution des instructions de ces sections.

Donc, pour imposer un ordre et agir sur la manière dont les différents threads vont accéder à ces données partagées ou aux sections de code qui sont sensibleS, on dit qu'il faut les synchroniser. La synchronisation consiste donc à imposer des règles de contrôle d'accès à des portions de code où on manipule des ressources partagées. Il existe plusieurs mécanismes de synchronisation, parmi lesquels les verrous, les sémaphores, les conditions, les moniteurs. Et ici, nous allons regarder le mécanisme le plus simple qui est le verrou. Le verrou est une structure qui, peut être, dans deux états, comme son nom l'indique verrouillé ou déverrouillé. Du coup, les opérations pour manipuler un verrou, c'est verrouiller qui est équivalent à prendre le verrou ou déverrouiller, où on relâche le verrou. Quand nous avons plusieurs threads qui utilisent le même verrou : (alors) quand un thread verrouille, il faut qu'il déverrouille au bout d'un certain temps. Un thread ne peut pas déverrouiller un verrou qu'il n'a pas au préalable verrouillé. (Alors) Quand on essaye de verrouiller, si c'est libre, si ce n'est pas verrouillé, alors un thread verrouille. Quand il déverrouille, un autre thread qui voudrait verrouiller pourrait le faire. Mais tant que ce n'est pas déverrouillé…

[00:04:21]

…ceux qui veulent prendre le verrou doivent attendre. Pour illustrer ce fonctionnement. imaginons la machine à café. La machine à café, typiquement, c'est une ressource convoitée et usuellement, c'est un usager à la fois. Alors, quand la machine est libre, on peut arriver, on est content, on est les premiers arrivés et on commence à se faire un café, donc on verrouille la machine. Pendant qu'on est en train de nous faire un café, donc, la machine n'est pas libre. il peut y avoir d'autres collègues qui arrivent. Du coup, même s'ils aiment bien papoter avec nous, ils ne vont pas être contents puisqu'il va falloir qu'ils attendent. que le premier usager finisse avec son café pour qu'ils puissent faire le leur. S'il y a encore d'autres collègues qui arrivent, ils attendent que la machine à café se libère. Quand l'usager a fini avec son café, il déverrouille la machine, est là, un des usagers qui attend peut lui commencer à se faire un café.

Pour notre exemple avec les threads qui font des opérations arithmétiques on peut appliquer donc le verrou. Il suffirait donc, avant de faire cette suite d'opérations, de verrouiller, et à la fin, quand on a fini, de déverrouiller. On est bien d'accord que tous les threads, ceux qui utilisent l'opération addition, donc les PlusThread et ceux qui font la soustraction, les MinusThread, doivent utiliser le même verrou. Si on fait cette chose là qui, en Python, se fait en utilisant le module threading, donc dans le thread principal, le thread initial, on définit un verrou.

[00:06:33]

On crée donc un objet threading.Lock et ce verrou va être utilisé dans le threadPlusThread. Donc, ici, on dit bien qu'on utilise le verrou qui est global et donc qui est partagé. Et avant de faire le +1, on dit je verrouille et après avoir fait le +1, on dit je déverrouille. La même chose du côté du thread qui fait les soustractions. Si donc on exécute ce programme modifié, on arrive bien à une liste qui ne contient que des zéros. Donc, nous n'avons plus les phénomènes non désirables de la séquence précédente. Toutefois, ça ne vient pas sans un coût à payer. En effet, quand on synchronise, on fait attendre les autres threads et si on regarde donc deux exécutions, une sans synchronisation, qui est celle du bas, et une avec synchronisation qui est celle du haut si on utilise la fonction time qui nous donne le temps global de l'exécution de notre programme, on voit qu'il y a une différence. Donc, avec synchronisation, c'est toujours plus lent.

Nous avions dit que la programmation multi thread est compliquée. En effet, avec les multithread, nous avons des problèmes de concurrence. Pour résoudre ces problèmes de concurrence, il nous faut de la synchronisation. Toutefois, quand on synchronise les thread, on induit des attentes et donc on ralentit l'exécution globale de notre programme. Il existe plusieurs mécanismes de synchronisation et nous avons vu ici le plus simple qui est le verrou.