Titre RNCP Niveau 7

Maîtrisez pipelines, cloud & IA pour devenir Data Engineer opérationnel.

Devenez Data Engineer

DataScientist.fr

Formations

L'équipeLa plateforme
Offre entreprises

🇫🇷

Introduction au threading en Python - Tutoriel interactif
Python

Introduction au threading en Python - Tutoriel interactif

Romain DE LA SOUCHÈRE

Lead Developer, Expert Cloud et DevOps

Publié le 6 janvier 2025 · 12 min de lecture

Dans le monde de la programmation concurrente, les threads jouent un rôle crucial en permettant l'exécution simultanée de tâches au sein d'un même programme. Cette capacité à gérer plusieurs opérations en parallèle optimise les performances et l'efficacité des applications modernes. Découvrez comment démarrer, manipuler et synchroniser des threads, tout en évitant les pièges courants tels que les accès concurrents. Plongez dans l'univers du threading et explorez les concepts essentiels pour maîtriser l'art de la programmation multithreadée.

Qu'est-ce qu'un thread ?

Un thread, ou fil d'exécution, est une unité de traitement qui fait partie d'un processus plus large. Il permet à un programme d'exécuter plusieurs opérations de manière concurrente à l'intérieur du même processus. Dans le contexte de Python, un thread est particulièrement utile pour les tâches qui peuvent être exécutées indépendamment les unes des autres, comme les opérations d'entrée/sortie ou les tâches de fond.

Comprendre les threads

Les threads sont souvent comparés aux processus, mais il y a des différences clés. Un processus est une instance d'un programme en cours d'exécution, et il possède sa propre mémoire. En revanche, un thread partage la mémoire et les ressources du processus principal, ce qui permet une communication et un échange de données plus rapides entre les threads. Cependant, cela peut également entraîner des problèmes de synchronisation et de concurrence, car plusieurs threads peuvent accéder et modifier les mêmes données simultanément.

L'utilisation des threads en Python

Python fournit le module threading qui facilite la gestion des threads. Voici un exemple simple de la création et de l'exécution d'un thread en Python :
python
Dans cet exemple, nous définissons une fonction afficher_message qui imprime un message. Nous créons ensuite un thread en utilisant threading.Thread, en spécifiant la fonction à exécuter. Le thread est démarré avec start() et nous utilisons join() pour attendre sa terminaison.

Avantages et inconvénients des threads

Avantages :
  • Concurrence : Les threads permettent l'exécution concurrente, ce qui peut optimiser l'utilisation des ressources du processeur.
  • Partage de mémoire : Ils partagent la mémoire du processus, ce qui facilite la communication entre eux.
Inconvénients :
  • Complexité accrue : La gestion de la synchronisation et des accès concurrents aux ressources partagées peut être complexe et sujette à des erreurs.
  • Global Interpreter Lock (GIL) : En Python, le GIL limite l'exécution des threads à un seul à la fois dans un processus, ce qui peut réduire les bénéfices du multithreading pour les tâches CPU-intensives.
Les threads sont une puissante composante de Python qui, lorsqu'ils sont utilisés correctement, peuvent améliorer l'efficience et la réactivité des programmes.

Démarrer un thread

Pour démarrer un thread en Python, il est essentiel de comprendre les principales étapes impliquées dans sa création et son exécution. Dans cette section, nous allons explorer comment initialiser et exécuter un thread en utilisant le module threading.

Création d'un thread

Le processus de création d'un thread commence par l'importation du module threading. Vous devez définir une fonction ou une méthode qui contiendra le code que vous souhaitez exécuter dans le thread. Cela peut être une fonction simple ou une méthode d'une classe personnalisée.
Voici un exemple basique :
python
Dans cet exemple, nous avons défini une fonction tache qui sera exécutée par le thread.

Démarrer le thread

Après la création du thread, vous devez l'exécuter en appelant la méthode start(). Cette méthode lance le thread et appelle la fonction cible spécifiée :
python
Lorsque start() est invoqué, le thread commence à s'exécuter de manière indépendante du thread principal. Cela signifie que votre programme principal peut continuer à s'exécuter en parallèle.

Joindre le thread

Une fois que vous avez démarré un thread, il est souvent utile d'attendre qu'il termine son exécution avant de poursuivre d'autres opérations. Vous pouvez accomplir cela avec la méthode join(), qui bloque le thread appelant jusqu'à ce que le thread sur lequel elle est appelée se termine :
python
Cette approche garantit que toutes les opérations dans le thread sont complètes avant de continuer avec le reste du programme.

Exemple concret

Considérons un exemple où nous utilisons un thread pour effectuer un calcul simple :
python
Dans cet exemple, le thread calcul_thread exécute une fonction qui calcule la somme des nombres de 0 à 999. Le programme principal attend la fin du thread avant d'imprimer le résultat.
Avec ces étapes, vous pouvez démarrer et gérer efficacement des threads dans vos applications Python, en tirant parti du parallélisme pour améliorer les performances et la réactivité.

Travailler avec plusieurs threads

Travailler avec plusieurs threads en Python permet d'optimiser l'exécution de tâches concurrentes, ce qui est particulièrement utile pour les opérations d'entrée/sortie ou les tâches indépendantes. Il est toutefois essentiel de gérer correctement la synchronisation pour éviter les conflits d'accès aux ressources partagées.

Création de plusieurs threads

Pour créer plusieurs threads, vous pouvez simplement instancier plusieurs objets Thread, chacun avec sa propre fonction cible. Voici un exemple :
python
Dans cet exemple, chaque thread affiche un message différent, et nous utilisons une liste pour gérer et démarrer plusieurs threads.

Synchronisation des threads

Lorsqu'on travaille avec des threads, il est important de gérer l'accès aux ressources partagées pour éviter des problèmes tels que les accès concurrents. Python fournit plusieurs mécanismes pour synchroniser les threads, y compris les verrous (Lock).
Voici comment utiliser un verrou pour protéger une section critique de code :
python
Dans cet exemple, nous utilisons un verrou pour s'assurer qu'une seule thread peut accéder à la variable compteur à la fois, prévenant ainsi les erreurs de simultanéité.

Avantages et défis

L'utilisation de plusieurs threads peut améliorer les performances pour les tâches d'entrée/sortie et les opérations indépendantes. Cependant, cela augmente également la complexité de gestion des ressources partagées et de la synchronisation, nécessitant une planification minutieuse pour éviter les problèmes de concurrence.

Utilisation d'un threadpoolexecutor

Pour gérer efficacement un grand nombre de threads, Python offre le module concurrent.futures avec la classe ThreadPoolExecutor. Cette classe simplifie la gestion des threads en fournissant un pool de threads réutilisables, optimisant ainsi l'utilisation des ressources.

Introduction à ThreadPoolExecutor

ThreadPoolExecutor permet d'exécuter des appels de fonction de manière asynchrone à l'aide d'un pool fixe de threads. Vous pouvez spécifier le nombre de threads à utiliser, et la classe s'occupe de la répartition des tâches.
Voici un exemple d'utilisation de ThreadPoolExecutor :
python
Dans cet exemple, nous créons un pool de trois threads et soumettons plusieurs tâches qui simulent des opérations longues. executor.submit() envoie une tâche à un thread disponible dans le pool.

Avantages du ThreadPoolExecutor

  • Gestion simplifiée : Vous n'avez pas besoin de gérer les threads individuellement. Le pool gère la création et la réutilisation des threads.
  • Scalabilité : En ajustant le nombre de threads dans le pool, vous pouvez optimiser les performances pour les tâches I/O sans surcharger le système.
  • Synchronisation automatique : ThreadPoolExecutor gère automatiquement la synchronisation des tâches, simplifiant le code et réduisant le risque d'erreurs.

Considérations

Bien que ThreadPoolExecutor simplifie la gestion des threads, il est crucial de comprendre la nature des tâches que vous exécutez. Pour des tâches CPU-intensives, il peut être plus efficace d'utiliser ProcessPoolExecutor qui utilise plusieurs processus au lieu de threads, contournant ainsi le Global Interpreter Lock (GIL) de Python.
L'utilisation de ThreadPoolExecutor est idéale pour des opérations I/O-intensives, comme les requêtes réseau, les lectures/écritures de fichiers, où la mise en attente est fréquente.

Accès concurrents (Race conditions)

Les accès concurrents (race conditions) représentent un défi majeur lors de l'utilisation de threads multiples. Elles surviennent lorsque deux ou plusieurs threads accèdent et manipulent des données partagées de manière concurrente sans synchronisation appropriée, menant potentiellement à des résultats imprévisibles ou erronés.

Exemple d'accès concurrents

Considérons un scénario où plusieurs threads augmentent une variable partagée :
python
Dans cet exemple, nous pourrions nous attendre à ce que la valeur finale du compteur soit 10 000. Cependant, en raison des accès concurrents, le résultat peut être inférieur, car plusieurs threads peuvent lire et écrire la variable compteur simultanément sans coordination.

Prévention des accès concurrents

Pour éviter ces problèmes, il est essentiel d'utiliser des mécanismes de synchronisation comme les verrous (Lock). Un verrou assure qu'une seule thread à la fois peut exécuter une section critique du code :
python
En utilisant un verrou, nous garantissons que l'opération d'incrémentation est atomique, empêchant ainsi les accès concurrents.

Importance de la synchronisation

La synchronisation est cruciale pour maintenir l'intégrité des données partagées. Bien qu'elle puisse introduire un surcoût en termes de performance, elle est nécessaire pour éviter des erreurs difficiles à diagnostiquer et à corriger dans des applications multithreadées. En comprenant et en appliquant correctement les mécanismes de synchronisation, vous pouvez concevoir des applications robustes et fiables.

Synchronisation de base avec lock

La synchronisation est une composante essentielle du développement multithreadé, permettant de contrôler l'accès aux ressources partagées. Le verrou (Lock) est l'un des outils les plus simples pour gérer cette synchronisation en Python.

Utilisation d'un verrou

Un verrou fonctionne comme un signal d'arrêt qui empêche les autres threads d'accéder à une section critique du code lorsqu'elle est déjà en cours d'utilisation par un thread. Voici comment utiliser un verrou pour synchroniser l'accès à une variable partagée :
python
Dans cet exemple, le verrou garantit que seul un thread peut modifier la variable compteur à la fois, empêchant ainsi les accès concurrents.

Fonctionnement du verrou

  • Acquérir le verrou : Avant d'entrer dans une section critique, un thread doit acquérir le verrou. Si un autre thread détient déjà le verrou, le thread en attente sera bloqué jusqu'à ce que le verrou soit libéré.
  • Libérer le verrou : Une fois la section critique terminée, le thread libère le verrou, permettant à d'autres threads d'accéder à la section.
En Python, l'utilisation du verrou avec le mot-clé with comme dans l'exemple ci-dessus simplifie le code en s'assurant que le verrou est toujours correctement libéré, même si une exception se produit.

Importance de la synchronisation de base

Même si l'utilisation de verrous peut introduire un léger ralentissement à cause du blocage des threads, elle est indispensable pour garantir l'intégrité des données dans une application multithreadée. Sans synchronisation appropriée, les applications peuvent produire des résultats inattendus ou erronés, particulièrement lorsque plusieurs threads modifient simultanément des ressources partagées.
En comprenant et en appliquant correctement les verrous, vous pouvez minimiser les risques associés aux accès concurrents et créer des applications plus robustes et fiables.

Threading producteur-consommateur

Le modèle producteur-consommateur est un paradigme classique de programmation concurrente qui divise les tâches entre les producteurs, qui génèrent des données, et les consommateurs, qui traitent ces données. En Python, ce modèle peut être implémenté efficacement en utilisant des threads et des files d'attente pour synchroniser le flux de données.

Mise en œuvre avec queue.Queue

La classe Queue du module queue est particulièrement utile pour gérer la communication entre producteurs et consommateurs. Elle offre une manière sécurisée de partager des données entre threads, évitant ainsi les accès concurrents.
Voici un exemple de mise en œuvre :
python
Dans cet exemple, le producteur génère cinq éléments, qu'il ajoute à la file d'attente. Le consommateur les retire au fur et à mesure, utilisant task_done() pour indiquer qu'une tâche a été traitée.

Avantages du modèle producteur-consommateur

  • Découplage : Les producteurs et les consommateurs peuvent fonctionner à des vitesses différentes sans problème, car la file d'attente agit comme un tampon.
  • Flexibilité : Il est facile de modifier le nombre de producteurs et de consommateurs pour adapter les performances selon les besoins de l'application.

Considérations

L'utilisation d'un thread consommateur en mode démon (daemon=True) assure que le programme se termine même si le consommateur n'a pas fini de traiter tous les éléments de la file d'attente. Cela est particulièrement utile pour les applications où il est acceptable que certains éléments ne soient pas traités si le programme doit s'arrêter rapidement.

Objets de threading

Le module threading de Python fournit plusieurs objets qui facilitent la gestion des threads et la synchronisation. Ces objets sont essentiels pour coordonner l'exécution de plusieurs threads et garantir la sécurité des données partagées.

Thread

L'objet de base du module threading est le Thread. Il représente une unité d'exécution distincte. Pour créer un thread, vous pouvez soit passer une fonction à l'argument target, soit sous-classer Thread et redéfinir la méthode run() :
python

Lock

Un Lock est un objet synchronisé qui permet de gérer l'accès concurrent aux ressources partagées. Il doit être acquis avant l'entrée dans une section critique et libéré après :
python

RLock

Un RLock (verrou réentrant) est similaire à un Lock, mais permet au même thread d'acquérir le verrou plusieurs fois sans bloquer. Cela est utile pour les fonctions récursives ou lorsque plusieurs acquisitions de verrou sont nécessaires :
python

Event

Un Event est un mécanisme de signalisation qui permet à un thread d'attendre qu'un événement se produise. Il est souvent utilisé pour la coordination entre threads :
python

Condition

Un Condition est un objet qui permet à un thread d'attendre jusqu'à ce qu'une certaine condition soit remplie. Il est souvent utilisé en combinaison avec un verrou :
python
Ces objets sont des outils puissants pour structurer et gérer des applications multithreadées, offrant des moyens flexibles et sécurisés pour synchroniser l'exécution des threads.

Partager avec

💙 Merci d'avoir parcouru l'article jusqu'à la fin !

Romain DE LA SOUCHÈRE

Romain DE LA SOUCHÈRE - Lead Developer, Expert Cloud et DevOps

Ingénieur de formation avec plus de 11 ans d'expérience dans le développement back-end et le data engineering. Expert dans l’industrialisation des projets data dans le cloud.

» En savoir plus

Formations associés

Toutes nos formations

Préparez la certification PL‑300
Préparez la certification PL‑300
24 heures
Débutant
Garantie
Préparez la certification AZ-900
Préparez la certification AZ-900
10 heures
Débutant
Garantie
Préparez la certification DP‑700
Préparez la certification DP‑700
24 heures
Débutant
Garantie
Préparez la certification DP‑900
Préparez la certification DP‑900
10 heures
Débutant
Garantie

DataScientist.fr

By AXI Technologies

128 Rue de la Boétie,
75008, Paris, France

bonjour@datascientist.fr

+33 1 70 39 08 31

+33 6 86 99 34 78

© 2026 DataScientist.fr - AXI Technologies - Tous droits réservés