Comment synchroniser les ressources partagées dans les threads Python

PythonPythonBeginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Les capacités de threading (multithreading) de Python permettent aux développeurs d'exploiter le pouvoir du traitement parallèle, mais la gestion des ressources partagées entre les threads peut être une tâche difficile. Ce tutoriel vous guidera tout au long du processus de synchronisation des données partagées dans les threads Python, en garantissant une exécution sûre pour les threads et l'intégrité des données.

Présentation des threads Python

Le module threading intégré à Python vous permet de créer et de gérer des threads (fil d'exécution), qui sont des unités légères d'exécution pouvant s'exécuter de manière concurrente au sein d'un même processus. Les threads sont utiles lorsque vous avez besoin d'effectuer plusieurs tâches simultanément, comme gérer des opérations d'E/S, traiter des données en arrière-plan ou répondre à plusieurs requêtes de clients.

Qu'est-ce que sont les threads Python?

Les threads sont des séquences d'exécution indépendantes au sein d'un même processus. Ils partagent le même espace mémoire, ce qui signifie qu'ils peuvent accéder et modifier les mêmes variables et structures de données. Cet accès partagé aux ressources peut entraîner des problèmes de synchronisation, que nous aborderons dans la section suivante.

Avantages de l'utilisation des threads

L'utilisation de threads en Python peut offrir plusieurs avantages, notamment :

  1. Réactivité améliorée : Les threads permettent à votre application de rester réactive tout en effectuant des tâches chronophages, telles que des opérations d'E/S ou des calculs longs.
  2. Parallélisme : Les threads peuvent tirer parti des processeurs multi-cœurs pour exécuter des tâches de manière concurrente, ce qui peut potentiellement améliorer les performances globales de votre application.
  3. Partage de ressources : Les threads au sein du même processus peuvent partager des données et des ressources, ce qui peut être plus efficace que de créer des processus séparés.

Défis potentiels liés aux threads

Bien que les threads puissent être puissants, ils introduisent également certains défis dont vous devez être conscient :

  1. Synchronisation : Lorsque plusieurs threads accèdent à des ressources partagées, vous devez vous assurer qu'ils ne s'interfèrent pas les uns avec les autres, ce qui peut entraîner des conditions de course (race conditions) et d'autres problèmes de synchronisation.
  2. Interblocage (Deadlocks) : Une gestion inappropriée des ressources partagées peut entraîner des interblocages, où deux threads ou plus attendent que les autres libèrent des ressources, rendant l'application non réactive.
  3. Sécurité des threads (Thread Safety) : Vous devez vous assurer que votre code est sûr pour les threads, c'est-à-dire qu'il peut être exécuté en toute sécurité par plusieurs threads sans causer de corruption de données ou d'autres problèmes.

Dans la section suivante, nous approfondirons le sujet de la synchronisation des ressources partagées dans les threads Python.

Synchronisation des données partagées

Lorsque plusieurs threads accèdent aux mêmes ressources partagées, telles que des variables ou des structures de données, cela peut entraîner des conditions de course (race conditions) et d'autres problèmes de synchronisation. Ces problèmes peuvent entraîner une corruption des données, un comportement inattendu ou même des plantages de votre application. Pour résoudre ces problèmes, Python propose plusieurs mécanismes pour synchroniser les données partagées.

Conditions de course (Race Conditions)

Une condition de course se produit lorsque le résultat final d'un calcul dépend du chronométrage relatif ou de l'entrelacement des opérations de plusieurs threads sur des données partagées. Cela peut entraîner des résultats imprévisibles et incorrects.

Considérez l'exemple suivant :

import threading

counter = 0

def increment_counter():
    global counter
    for _ in range(1000000):
        counter += 1

threads = []
for _ in range(2):
    t = threading.Thread(target=increment_counter)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Final counter value: {counter}")

Dans cet exemple, deux threads incrémentent une variable counter partagée 1 000 000 de fois chacun. Cependant, en raison des conditions de course, la valeur finale de counter peut ne pas être de 2 000 000 comme prévu.

Primitives de synchronisation

Le module threading de Python propose plusieurs primitives de synchronisation pour vous aider à gérer l'accès aux ressources partagées :

  1. Verrous (Locks) : Les verrous sont la primitive de synchronisation la plus basique. Ils vous permettent de vous assurer qu'un seul thread peut accéder à une section critique de code à la fois.
  2. Sémaphores (Semaphores) : Les sémaphores sont utilisés pour contrôler l'accès à un nombre limité de ressources.
  3. Variables de condition (Condition Variables) : Les variables de condition permettent aux threads d'attendre que certaines conditions soient remplies avant de poursuivre leur exécution.
  4. Événements (Events) : Les événements sont utilisés pour signaler à un ou plusieurs threads qu'un événement particulier s'est produit.

Ces primitives de synchronisation peuvent être utilisées pour vous assurer que vos threads accèdent aux ressources partagées de manière sûre et coordonnée, en évitant les conditions de course et d'autres problèmes de synchronisation.

graph LR A[Thread 1] --> B[Acquire Lock] B --> C[Critical Section] C --> D[Release Lock] E[Thread 2] --> F[Acquire Lock] F --> G[Critical Section] G --> H[Release Lock]

Dans la section suivante, nous explorerons des exemples pratiques d'utilisation de ces techniques de synchronisation dans vos applications Python.

Techniques pratiques de synchronisation de threads

Maintenant que nous avons couvert les concepts de base de la synchronisation des données partagées dans les threads Python, explorons quelques exemples pratiques d'utilisation des différentes primitives de synchronisation.

Utilisation des verrous (Locks)

Les verrous sont la primitive de synchronisation la plus basique en Python. Ils garantissent qu'un seul thread peut accéder à une section critique de code à la fois. Voici un exemple d'utilisation d'un verrou pour protéger un compteur partagé :

import threading

counter = 0
lock = threading.Lock()

def increment_counter():
    global counter
    for _ in range(1000000):
        with lock:
            counter += 1

threads = []
for _ in range(2):
    t = threading.Thread(target=increment_counter)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Final counter value: {counter}")

Dans cet exemple, l'objet lock est utilisé pour s'assurer qu'un seul thread peut accéder à la section critique de code où la variable counter est incrémentée.

Utilisation des sémaphores (Semaphores)

Les sémaphores sont utilisés pour contrôler l'accès à un nombre limité de ressources. Voici un exemple d'utilisation d'un sémaphore pour limiter le nombre de connexions simultanées à une base de données :

import threading
import time

database_connections = 3
connection_semaphore = threading.Semaphore(database_connections)

def use_database():
    with connection_semaphore:
        print(f"{threading.current_thread().name} acquired a database connection.")
        time.sleep(2)  ## Simulating database operation
        print(f"{threading.current_thread().name} released a database connection.")

threads = []
for _ in range(5):
    t = threading.Thread(target=use_database)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Dans cet exemple, le connection_semaphore est utilisé pour limiter le nombre de connexions simultanées à la base de données à 3. Chaque thread doit obtenir un « permis » du sémaphore avant de pouvoir utiliser une connexion à la base de données.

Utilisation des variables de condition (Condition Variables)

Les variables de condition permettent aux threads d'attendre que certaines conditions soient remplies avant de poursuivre leur exécution. Voici un exemple d'utilisation d'une variable de condition pour coordonner la production et la consommation d'éléments dans une file d'attente :

import threading
import time

queue = []
queue_size = 5
queue_condition = threading.Condition()

def producer():
    with queue_condition:
        while len(queue) == queue_size:
            queue_condition.wait()
        queue.append(1)
        print(f"{threading.current_thread().name} produced an item. Queue size: {len(queue)}")
        queue_condition.notify_all()

def consumer():
    with queue_condition:
        while not queue:
            queue_condition.wait()
        item = queue.pop(0)
        print(f"{threading.current_thread().name} consumed an item. Queue size: {len(queue)}")
        queue_condition.notify_all()

producer_threads = [threading.Thread(target=producer) for _ in range(2)]
consumer_threads = [threading.Thread(target=consumer) for _ in range(3)]

for t in producer_threads + consumer_threads:
    t.start()

for t in producer_threads + consumer_threads:
    t.join()

Dans cet exemple, la variable queue_condition est utilisée pour coordonner la production et la consommation d'éléments dans une file d'attente. Les producteurs attendent que de l'espace soit disponible dans la file d'attente, tandis que les consommateurs attendent qu'il y ait des éléments dans la file d'attente.

Ces exemples démontrent comment vous pouvez utiliser les différentes primitives de synchronisation fournies par le module threading de Python pour gérer efficacement les ressources partagées et éviter les problèmes courants de concurrence.

Résumé

Dans ce tutoriel Python complet, vous apprendrez à synchroniser efficacement les ressources partagées dans vos applications multithreadées. En comprenant les primitives de synchronisation intégrées à Python, telles que les verrous (locks), les sémaphores (semaphores) et les variables de condition (condition variables), vous pourrez coordonner l'accès concurrent et éviter les conditions de course (race conditions), garantissant ainsi la stabilité et la fiabilité de vos programmes Python.