dernière modification : 2024

Multi-threading en python

Le multi-threading consiste à écrire un programme composé de plusieurs fils d’exécution (thread). Chaque thread et matérialisé par une suite d’instructions qui lui est propre. L’exécution d’un tel programme donne lieu à plusieurs tâches qui progressent en même temps.

Un thread est léger, car son contexte se réduit à ses instructions, à un compteur ordinal qui pointe sur la prochaine instruction à exécuter et à la pile des appels de fonctions. Il est donc rapide à créer et à détruire, Sa gestion consomme peu de ressources (pas d’overhead). Il partage la mémoire avec les autres threads, donc son contexte ne contient pas les variables (d’où sa légèreté). La commutation de threads est donc rapide, mais en contre partie leur isolation est faible : ils partagent beaucoup (leurs variables en particulier).

Le cas de Python

Le module threading est l’un des moyens de faire du multi-threading, dans lequel le programmeur crée explicitement les threads sous forme de classe. Pour le programmeur, le thread est un concept abstrait : il peut en créer autant qu’il le souhaite dans son programme. Ensuite, au moment de l’exécuter, le programme est confronté au matériel : suivant l’ordinateur cible, un certain nombre de threads seront ou non distribués sur un certain nombre de coeurs (et à partir de 2 cœurs, il y aura parallélisme).

1. Tâche

Créer une tâche consiste à créer une sous-classe de threading.Thread. Cette sous-classe doit être dotée d’une méthode run implémentant le traitement à réaliser. Elle hérite d’une méthode start destinée à lancer l’exécution de la tâche. Exemple :

import threading
...

class MyThread(threading.Thread):
    def __init__(self, ...):
        threading.Thread.__init__(self)
        ...

    def run(self):
        ...

if __name__ == '__main__':
  mytask = MyThread(...)
  mytask.start()
...

2. Synchronisation

Synchroniser 2 tâches à un instant t, c’est définir un point de rendez-vous. La méthode join de la classe threading.Thread attend que la tâche courante se termine pour poursuivre le fil d’exécution.

3. Atomicité

Dans la vraie vie, l’incrémentation d’une variable mémoire n’est pas atomique. Elle se décompose en plusieurs opérations élémentaires :

  1. transfert de la valeur de la variable de la mémoire vers l’unité de calcul,

  2. incrémentation de la valeur dans l’unité de calcul,

  3. transfert de la nouvelle valeur de l’unité de calcul vers la mémoire.

4. Verrou

Les tâches se partageant les variables globales du programme, des interférences peuvent avoir lieu : par exemple, 2 tâches peuvent vouloir modifier en même temps la même variable. Pour éviter les effets indésirables dans ce genre de situation, il est nécessaire qu’une tâche puisse définir des portions de code in-interruptibles (sections critiques).

On crée une section critique en posant un verrou, et on en sort en le supprimant. Le verrou est une instance de la classe Lock. Pour créer une section critique, il faut donc :

  1. créer un verrou

  2. appeller la méthode acquire en début de section critique (poser le verrou)

  3. appeller la méthode release en fin de section critique (enlever le verrou)

Exemple
mon_verrou = threading.Lock() (1)
... (2)
mon_verrou.acquire() (3)
... (4)
mon_verrou.release() (5)
... (6)
1 création d’un verrou
2 section "normale" interruptible
3 début de section critique
4 la section critique
5 fin de la section critique
6 section "normale" interruptible

5. Particularité de l’interpréteur Python

Le global interpreter lock (GIL) est un verrou que l’interpréteur Python utilise en permanence pour se prémunir des accès concurrents à certaines de ses variables. Conséquence : à un instant t, un seul thread peut accèder à l’interpréteur. Ce qui pénalise le parallélisme.

Par conséquent, étant donnée l’implementation actuelle de l’interpréteur Python, le multi-threading est inefficace pour les programmes CPU bounds. Son intérêt se révèle pour des applications I/O bounds, comme dans le contexte des applications réseaux ou graphiques (programmation évènementielle d’une application en environnement graphique).

La suppression du GIL pour les futures versions de Python fait actuellement (2023) débat dans la communauté des développeurs Python.