Thread in Python – Multithreading (parte 3)

In questa terza parte della serie Thread in Python, vedremo alcuni aspetti del multithreading. Nella realtà infatti i thread possono essere molto diversi tra di loro e spesso i metodi di ricorsione per crearli e gestirli, come i loop for, non possono essere più utilizzabili. Esistono quindi degli strumenti che permettono di gestire diversi thread come ThreadPoolExecutor. La gestione dei thread rimane comunque un’operazione complessa che se non ben gestita può portare a problematiche come la Race Condition. In questo articolo vedremo in dettaglio questi due aspetti.

Thread in Python - Multithreading (part 3)

ThreadPoolExecutor

Quando i thread cominciano ad essere tanti, un modo efficiente per gestirli è il ThreadPoolExecutor. Questa interfaccia appartiene al modulo concurrent.futures e si crea come un context manager, utilizzando la dichiarazione with.

Il programma precedente, compreso di join per tutti i thread, si può trasformare nel seguente.

 import concurrent.futures
 import time
  
 def thread1():
     print("Thread 1 started")
     time.sleep(10)
     print("Thread 1 ended")
  
 def thread2():
     print("Thread 2 started")
     time.sleep(4)
     print("Thread 2 ended")
  
 print("Main start")
  
 with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
   executor.submit(thread1)
   executor.submit(thread2)
  
 print("Main end") 

Il risultato di esecuzione sarà lo stesso, solo che la gestione dei diversi thread viene eseguita all’interno del context manager di ThreadPoolExecutor, senza più dover definire, sia singolarmente, sia all’interno di iterazioni, la chiamata alle funzioni start() e join().

Thread in Python - Multiple threading

Race Conditions

Un altro concetto legato al threading è quello delle race conditions.

Queste particolari condizioni avvengono quando due o più thread accedono ad una serie di dati o risorse condivise. Se non ben gestito, l’accesso e la modifica da parte di un thread a queste risorse, può portare a risultati non coerenti.

Per semplicità di esempio, utilizzeremo delle variabili condivise come risorse. Nei casi reali, le race conditions avvengono quando più thread vogliono accedere agli stessi dati presenti su di un database.

Per simulare in un certo modo questa condizione, creeremo una variabile contenente un valore (risorsa condivisa) all’interno di un oggetto di una classe FakeDatabase, che simula un database (molto lontani dalla realtà, ma basta un po’ di fantasia…. 😉).

Inseriremo all’interno di questa variabile un valore di partenza uguale a 0. Poi creeremo tre diversi thread che avranno il compito di incrementare, ciascuno di essi, questo valore di 1, accedendo a questo ipotetico database. Al termine di questa operazione (programma) ci dovremmo aspettare un valore di 3 come risultato.

Inseriamo il codice seguente

 import concurrent.futures
 import time
  
 class FakeDatabase:
   def __init__(self):
     self.value = 0
  
   def update(self, name):
     print("Thread ", name , "is reading the DB value")
     local_copy = self.value
     local_copy += 1
     time.sleep(0.1)
     self.value = local_copy
     print("Thread ", name ,"has modified the DB value")
  
 database = FakeDatabase()
  
 with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
   for index in range(3):
     executor.submit(database.update, index+1)
  
 print("DB Value is ", database.value) 

Eseguendo il codice, otterremo:

Thread in Python - Race condition

Come possiamo vedere il valore finale sul DB è 1. Quando in realtà dovrebbe essere 3, dato che il contributo di ciascun thread è di 1. Ma purtroppo ciascuno di essi ha acquisito il valore iniziale.

Abbiamo avuto quindi una condizione di race condition, in cui i tre thread hanno lavorato in maniera concorrenziale, con ciascuno la propria versione locale del valore letto dal database (local_copy) acquisita in maniera concorrenziale (indeterminata).

Nel nostro esempio, tutti e tre i thread accedono allo stesso valore iniziale, senza attendere che gli altri apportino il loro contributo. Effettueranno le loro operazioni indipendentemente in locale ( local_copy incrementata di uno) . E poi sovrascriveranno il valore sul database.

Così come strutturato che i thread siano 3 o uno solo, è praticamente la stessa cosa.

Nella realtà i casi sono molto più complessi, e le evidenze di race condition possono assumere le più svariate forme, con conseguenze di comportamenti anomali e risultati inattesi.

Conclusioni

In questa terza parte abbiamo visto i primi casi di Multi threading con l’introduzione della problematica Race Condition ed il tool ThreadPoolExecutor che permette di gestire al meglio più thread contemporaneamente. Nella parte seguente, approfondiremo con altri aspetti il Multi threading, come l’uso dei Lock nei thread e la problematica dei deadlock.

Lascia un commento