Python offre des capacités de threading puissantes pour améliorer les performances des applications grâce à l’exécution concurrente des tâches. Cependant, un threading non contrôlé peut conduire à une contention des ressources et à une inefficacité. Cet article explore des techniques de threading efficaces utilisant des files d’attente en Python, en se concentrant sur la prévention des pièges courants et l’optimisation des performances.
Table des matières
- Threads en Python
- Gestion des threads avec les files d’attente
- Choisir la bonne approche : Threads vs. Multitraitement
Threads en Python
Les threads Python permettent l’exécution apparemment simultanée de plusieurs fonctions. Ceci est particulièrement avantageux pour les opérations liées à l’E/S (requêtes réseau, traitement de fichiers), où un thread peut attendre des ressources externes sans bloquer les autres. Cependant, le Global Interpreter Lock (GIL) dans CPython limite le véritable parallélisme pour les tâches liées au CPU ; un seul thread peut détenir le contrôle de l’interpréteur Python à tout moment. Par conséquent, l’efficacité du threading est principalement réalisée avec les opérations liées à l’E/S.
Considérez cet exemple de threading simple sans file d’attente :
import threading
import time
def worker(name):
print(f"Thread {name}: démarrage")
time.sleep(2) # Simulation d'une opération liée à l'E/S
print(f"Thread {name}: fin")
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
print("Tous les threads ont terminé")
Ceci crée cinq threads, chacun exécutant la fonction worker
. Bien que fonctionnel, il manque de contrôle sur le nombre de threads concurrents, pouvant surcharger le système avec de nombreuses tâches.
Gestion des threads avec les files d’attente
Pour contrôler l’exécution concurrente des threads et éviter l’épuisement des ressources, utilisez queue.Queue
. La file d’attente agit comme un tampon, gérant les tâches entre le pool de threads et le traitement. Les threads récupèrent continuellement les tâches, les traitant jusqu’à ce que la file d’attente soit vide. Cette approche régule la concurrence et gère efficacement les ressources.
Voici un exemple amélioré utilisant queue.Queue
:
import threading
import time
import queue
def worker(q):
while True:
try:
item = q.get(True, 1) # Bloquer pendant 1 seconde, lever une exception si vide
print(f"Thread {threading.current_thread().name}: traitement de {item}")
time.sleep(2) # Simulation d'une opération liée à l'E/S
print(f"Thread {threading.current_thread().name}: fin de {item}")
q.task_done()
except queue.Empty:
break
q = queue.Queue()
num_threads = 3 # Contrôle des threads concurrents
for i in range(10): # Nombre de tâches
q.put(i)
threads = []
for i in range(num_threads):
t = threading.Thread(target=worker, args=(q,), daemon=True) # Les threads démon quittent lorsque le thread principal quitte
threads.append(t)
t.start()
q.join() # Attendre que toutes les éléments de la file d'attente soient traités
print("Toutes les tâches sont terminées")
Cet exemple utilise queue.Queue
pour contenir les tâches (0-9). Seuls trois threads s’exécutent concurremment, tirant de la file d’attente. q.join()
garantit que le thread principal attend l’achèvement de la tâche. daemon=True
fait en sorte que les threads worker se terminent lorsque le thread principal se termine, empêchant les blocages.
Choisir la bonne approche : Threads vs. Multitraitement
Cette approche améliorée offre un meilleur contrôle, une meilleure gestion des ressources et une meilleure évolutivité. N’oubliez pas que pour les tâches liées au CPU, le multitraitement (à l’aide du module multiprocessing
) est généralement plus efficace dans CPython que le threading en raison de la limitation du GIL. Choisissez l’approche appropriée en fonction du fait que vos tâches sont liées à l’E/S ou au CPU.