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}")