Python : Création de Threads par Héritage et Gestion de la Concurrence

Il existe pulsieurs approches pour créer des threads en Python. Outre la création fonctionnelle, l'héritage offre une méthode orientée objet pour gérer les threads.

Création de Threads par Héritage

Pour créer un thread en utiilsant l'héritage, vous définissez une classe qui hérite de threading.Thread. La logique du thread est implémentée dans la méthode run().


import threading
import time

class MyThread(threading.Thread):
    def __init__(self, thread_id, name, counter):
        threading.Thread.__init__(self)
        self.threadID = thread_id
        self.name = name
        self.counter = counter

    def run(self):
        print(f"Starting {self.name}")
        print_time(self.name, self.counter, 5)
        print(f"Exiting {self.name}")

def print_time(threadName, delay, counter):
    while counter:
        time.sleep(delay)
        print(f"{threadName}: {time.ctime(time.time())}")
        counter -= 1

if __name__ == "__main__":
    thread1 = MyThread(1, "Thread-1", 1)
    thread2 = MyThread(2, "Thread-2", 2)
    thread1.start()
    thread2.start()

Gestion de la Concurrence avec les Verrous

Lors de l'accès à des ressources partagées par plusieurs threads, des problèmes de concurrence peuvent survenir, conduisant à des résultats inattendus. Les verrous (Locks) sont utilisés pour synchroniser l'accès à ces ressources.

Scénario sans Verrou

Considérons un scénario où plusieurs threads tentent de décrémenter une variable globale partagée.


import threading
import time

# Variable globale partagée
shared_variable = 100

def decrement_shared_variable():
    global shared_variable
    # Opération potentiellement non atomique
    temp = shared_variable
    time.sleep(0.01) # Simule une latence ou une interruption du CPU
    shared_variable = temp - 1

threads = []
for _ in range(100):
    thread = threading.Thread(target=decrement_shared_variable)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"Final value of shared_variable (without lock): {shared_variable}")
# Le résultat peut varier (ex: 99, 25, etc.)

Dans cet exemple, l'opération shared_variable = temp - 1 peut être interrompue entre la lecture de shared_variable (dans temp) et l'écriture de la nouvelle valeur. Si plusieurs threads lisent la même valeur avant qu'aucun n'ait fini d'écrire, des mises à jour peuvent être perdues.

Solution : Utilisation de threading.Lock

Un threading.Lock garantit qu'un seul thread peut accéder à la section critique du code à la fois.


import threading
import time

shared_variable = 100
lock = threading.Lock() # Création d'un objet Lock

def safe_decrement_shared_variable():
    global shared_variable
    lock.acquire() # Acquisition du verrou avant d'accéder à la ressource partagée
    try:
        temp = shared_variable
        time.sleep(0.01)
        shared_variable = temp - 1
    finally:
        lock.release() # Libération du verrou, permettant à d'autres threads d'y accéder

threads = []
for _ in range(100):
    thread = threading.Thread(target=safe_decrement_shared_variable)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"Final value of shared_variable (with lock): {shared_variable}")
# Le résultat sera toujours 0

Interblocage (Deadlock) et Verrous Récursifs

Un interblocage se produit lorsque deux ou plusieurs threads attendent indéfiniment l'un de l'autre pour libérer des ressourcse (verrous).

Exemple d'Interblocage


import threading
import time

lock_a = threading.Lock()
lock_b = threading.Lock()

class DeadlockThread(threading.Thread):
    def method_a(self):
        print(f"{self.name}: Trying to acquire lock_a...")
        lock_a.acquire()
        print(f"{self.name}: Acquired lock_a.")
        time.sleep(1)
        print(f"{self.name}: Trying to acquire lock_b...")
        lock_b.acquire()
        print(f"{self.name}: Acquired lock_b.")
        lock_b.release()
        print(f"{self.name}: Released lock_b.")
        lock_a.release()
        print(f"{self.name}: Released lock_a.")

    def method_b(self):
        print(f"{self.name}: Trying to acquire lock_b...")
        lock_b.acquire()
        print(f"{self.name}: Acquired lock_b.")
        time.sleep(1)
        print(f"{self.name}: Trying to acquire lock_a...")
        lock_a.acquire()
        print(f"{self.name}: Acquired lock_a.")
        lock_a.release()
        print(f"{self.name}: Released lock_a.")
        lock_b.release()
        print(f"{self.name}: Released lock_b.")

    def run(self):
        self.method_a()
        self.method_b()

threads = []
for i in range(2):
    thread = DeadlockThread(name=f"Thread-{i+1}")
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()
# Ce code peut potentiellement se bloquer.

Solution : Utilisation de threading.RLock (Verrou Récursif)

Un RLock (verrou récursif) peut être acquis plusieurs fois par le même thread. Il maintient un compteur d'acquisition/libération.


import threading
import time

recursive_lock = threading.RLock() # Création d'un verrou récursif

class RecursiveLockThread(threading.Thread):
    def method_a(self):
        print(f"{self.name}: Trying to acquire recursive_lock (level 1)...")
        recursive_lock.acquire()
        print(f"{self.name}: Acquired recursive_lock (level 1).")
        time.sleep(1)
        self.method_b_inner() # Appel d'une autre méthode qui utilise le même verrou
        recursive_lock.release()
        print(f"{self.name}: Released recursive_lock (level 1).")

    def method_b_inner(self):
        print(f"{self.name}: Trying to acquire recursive_lock (level 2)...")
        recursive_lock.acquire()
        print(f"{self.name}: Acquired recursive_lock (level 2).")
        time.sleep(1)
        recursive_lock.release()
        print(f"{self.name}: Released recursive_lock (level 2).")

    def run(self):
        self.method_a()

threads = []
for i in range(2):
    thread = RecursiveLockThread(name=f"Thread-{i+1}")
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()
# Ce code s'exécutera sans interblocage.

Gestion des Verrous dans les Classes

Pour gérer les ressources internes d'une classe de manière thread-safe, vous pouvez associer un verrou à chaque instance de la classe ou utiliser un verrou commun.

Approche avec un Verrou par Instance

Chaque instance de compte bancaire pourrait avoir son propre verrou.


import threading
import time

class Account:
    def __init__(self, id, initial_balance):
        self.id = id
        self.balance = initial_balance
        self._lock = threading.Lock() # Verrou spécifique à cette instance

    def withdraw(self, amount):
        with self._lock: # Utilisation de 'with' pour gérer automatiquement acquire/release
            if self.balance >= amount:
                print(f"{self.id}: Withdrawing {amount}...")
                time.sleep(0.05) # Simuler une opération
                self.balance -= amount
                print(f"{self.id}: New balance: {self.balance}")
            else:
                print(f"{self.id}: Insufficient funds for withdrawal of {amount}. Balance: {self.balance}")

    def deposit(self, amount):
        with self._lock:
            print(f"{self.id}: Depositing {amount}...")
            time.sleep(0.05) # Simuler une opération
            self.balance += amount
            print(f"{self.id}: New balance: {self.balance}")

    def transfer(self, to_account, amount):
        # Pour un transfert sécurisé, il faut verrouiller les deux comptes.
        # L'ordre des verrous est crucial pour éviter les interblocages.
        # Verrouiller d'abord l'objet le plus petit (ou selon un ordre défini).
        lock1, lock2 = sorted([self._lock, to_account._lock], key=id)

        with lock1:
            with lock2:
                print(f"Transferring {amount} from {self.id} to {to_account.id}...")
                if self.balance >= amount:
                    self.balance -= amount
                    to_account.balance += amount
                    print(f"Transfer successful. {self.id} balance: {self.balance}, {to_account.id} balance: {to_account.balance}")
                else:
                    print(f"Transfer failed: Insufficient funds in {self.id}.")

# Création des comptes
account1 = Account("Acc-1", 1000)
account2 = Account("Acc-2", 500)

# Création des threads pour les transferts
transfer_thread1 = threading.Thread(target=account1.transfer, args=(account2, 100))
transfer_thread2 = threading.Thread(target=account2.transfer, args=(account1, 50))

transfer_thread1.start()
transfer_thread2.start()

transfer_thread1.join()
transfer_thread2.join()

print(f"Final balance of {account1.id}: {account1.balance}")
print(f"Final balance of {account2.id}: {account2.balance}")

Étiquettes: Python Threads threading Concurrence Verrous

Publié le 25 juillet à 15h52