
Introduction au multithreading (module threading)
Découvrez le multithreading en Python avec le module 'threading'. Apprenez à créer et à gérer des threads, à exécuter des tâches en parallèle, et à comprendre les concepts de base de la concurrence.
Qu'est-ce que le multithreading ? Définition et avantages
Le multithreading est une technique de programmation qui permet à un programme d'exécuter plusieurs tâches (threads) *apparemment* en parallèle, au sein d'un même processus.
Un thread (ou fil d'exécution) est une séquence d'instructions qui peut être exécutée indépendamment des autres threads. Tous les threads d'un même processus partagent le même espace mémoire.
Le multithreading peut améliorer les performances des programmes qui effectuent des tâches indépendantes, en particulier les tâches qui sont limitées par des entrées/sorties (I/O-bound), comme :
- Attendre des données provenant d'un réseau.
- Lire ou écrire des fichiers.
- Attendre des entrées utilisateur.
Pendant qu'un thread attend une opération d'E/S, un autre thread peut continuer à s'exécuter, ce qui permet d'utiliser plus efficacement le processeur.
Il est important de noter que le multithreading en Python est limité par le Global Interpreter Lock (GIL), qui empêche plusieurs threads d'exécuter du code Python en *parallèle* sur plusieurs coeurs de processeur. Cependant, le multithreading reste utile pour les tâches limitées par les E/S, et il peut simplifier la conception de certains programmes.
Le module threading : créer et gérer des threads
Le module `threading` de la bibliothèque standard Python fournit les outils pour créer et gérer des threads.
Les classes et fonctions principales du module `threading` sont :
- `Thread` : La classe qui représente un thread.
- `start()` : Démarre l'exécution du thread.
- `join()` : Attend que le thread se termine.
- `Lock` : Un objet de synchronisation (verrou) qui permet d'éviter les problèmes d'accès concurrents aux ressources partagées.
- `RLock` : Un verrou réentrant.
- `Condition` : Une variable de condition, pour synchroniser les threads en fonction d'événements.
- `Semaphore` : Un sémaphore, pour limiter le nombre de threads qui peuvent accéder à une ressource simultanément.
- `Event` : Un mécanisme simple de communication entre threads.
- `Timer` : Un thread qui exécute une fonction après un certain délai.
Créer un thread : la classe Thread
Pour créer un thread, vous devez créer une instance de la classe `Thread` du module `threading`. Vous devez spécifier la fonction que le thread doit exécuter (la "cible" du thread).
Syntaxe :
import threading
# Définir la fonction que le thread exécutera
def ma_fonction(arguments):
# Code à exécuter dans le thread
# ...
# Créer un objet Thread
thread = threading.Thread(target=ma_fonction, args=(arg1, arg2, ...), kwargs={...})
# Démarrer le thread
thread.start()
# Attendre que le thread se termine (optionnel)
thread.join()- `target=ma_fonction` : Spécifie la fonction que le thread doit exécuter.
- `args=(arg1, arg2, ...)` : (Optionnel) Spécifie les arguments positionnels à passer à la fonction.
- `kwargs={...}` : (Optionnel) Spécifie les arguments nommés à passer à la fonction.
- `thread.start()` : Démarre l'exécution du thread. La fonction `ma_fonction` sera exécutée dans un thread séparé.
- `thread.join()` : (Optionnel) Attend que le thread se termine. Si vous ne l'appelez pas, le programme principal peut se terminer avant que le thread ne soit terminé.
Exemple :
import threading
import time
def afficher_nombres(n):
for i in range(n):
print(f"Thread : {i}")
time.sleep(0.1) # Simule une opération qui prend du temps
# Crée un thread qui exécutera la fonction afficher_nombres avec l'argument 5
thread = threading.Thread(target=afficher_nombres, args=(5,))
# Démarre le thread
thread.start()
# Le programme principal continue à s'exécuter en parallèle
for i in range(3):
print(f"Main : {i}")
time.sleep(0.2)
# Attend que le thread se termine
thread.join()
print("Fin du programme")Dans cet exemple, la fonction `afficher_nombres` est exécutée dans un thread séparé. Le programme principal et le thread s'exécutent en parallèle (de manière concurrente). L'instruction `time.sleep` est utilisée pour simuler des opérations qui prennent du temps et forcer l'alternance.
Exemple : téléchargement de fichiers en parallèle
Voici un exemple plus concret : un programme qui télécharge plusieurs fichiers en parallèle, en utilisant un thread pour chaque fichier :
import threading
import urllib.request
def telecharger_fichier(url, nom_fichier):
"""Télécharge un fichier depuis une URL et l'enregistre localement."""
try:
print(f"Téléchargement de {url}...")
urllib.request.urlretrieve(url, nom_fichier)
print(f"Téléchargement de {url} terminé.")
except Exception as e:
print(f"Erreur lors du téléchargement de {url} : {e}")
# Liste des fichiers à télécharger (URL, nom de fichier local)
fichiers = [
("https://www.exemple.com/fichier1.txt", "fichier1.txt"),
("https://www.exemple.com/fichier2.jpg", "fichier2.jpg"),
("https://www.exemple.com/fichier3.zip", "fichier3.zip"),
]
# Crée et démarre un thread pour chaque fichier
threads = []
for url, nom_fichier in fichiers:
thread = threading.Thread(target=telecharger_fichier, args=(url, nom_fichier))
thread.start()
threads.append(thread)
# Attend que tous les threads se terminent
for thread in threads:
thread.join()
print("Tous les téléchargements sont terminés.")Dans cet exemple :
- La fonction `telecharger_fichier` télécharge un fichier depuis une URL et l'enregistre localement.
- Une liste de tuples `(URL, nom_fichier)` définit les fichiers à télécharger.
- Une boucle `for` crée un thread pour chaque fichier, en utilisant la fonction `telecharger_fichier` comme cible et les arguments appropriés.
- Les threads sont démarrés avec `start()`.
- Une autre boucle `for` attend que tous les threads se terminent avec `join()`.
Le téléchargement des fichiers se fait en parallèle (ou plutôt, de manière concurrente), ce qui peut être beaucoup plus rapide que de les télécharger séquentiellement (l'un après l'autre).
Limitations du multithreading en Python : le GIL
Il est important de comprendre que le multithreading en Python a une limitation importante : le Global Interpreter Lock (GIL).
Le GIL est un mécanisme interne à l'interpréteur CPython (l'implémentation standard de Python) qui garantit qu'un seul thread natif exécute du code bytecode Python à un moment donné. Même sur une machine multi-coeur.
Cela signifie que le multithreading en Python ne permet pas d'obtenir un vrai parallélisme pour les tâches qui sont *CPU-bound* (limitées par la vitesse du processeur). Si votre programme passe la plupart de son temps à effectuer des calculs intensifs en Python, le multithreading n'améliorera pas les performances, et pourrait même les dégrader légèrement (à cause du surcoût de la gestion des threads).
Cependant, le GIL est relâché pendant les opérations d'E/S (entrée/sortie), comme la lecture/écriture de fichiers, les communications réseau, etc. Cela signifie que le multithreading peut être *très efficace* pour les tâches qui sont *I/O-bound* (limitées par la vitesse des entrées/sorties).
Si vous avez besoin de vrai parallélisme pour des tâches CPU-bound, vous devez utiliser le module `multiprocessing` (qui crée des processus séparés, contournant ainsi le GIL) ou des bibliothèques qui utilisent du code natif (comme NumPy) et qui relâchent le GIL.
En résumé : le multithreading en Python est utile pour les tâches I/O-bound, mais pas pour les tâches CPU-bound (à cause du GIL).