Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Multithreading

Implementare i thread su Python al fine di creare applicazioni concorrenti, sfruttando le funzionalità del modulo threading.
Implementare i thread su Python al fine di creare applicazioni concorrenti, sfruttando le funzionalità del modulo threading.
Link copiato negli appunti

Quando un'applicazione richiede l'esecuzione di task molto lunghi, nonché di operazioni potenzialmente parallelizzabili, è generalmente utilizzata la programmazione concorrente, a cui ci spesso si riferisce spesso con il termine "multithreading".

Un'applicazione che sfrutta questo paradigma si dice multithread. I thread non sono altro che sotto-processi eseguiti generalmente in parallelo, e generati da un processo padre. Quando uno script Python viene eseguito, abbiamo infatti la possibilità di creare uno o più thread, che possono collaborare per il raggiungimento di uno scopo comune, eventualmente condividendo le stesse risorse computazionali, nonché gli stessi dati.

I processori dei computer più recenti sono generalmente multi-core, offrendo quindi la possibilità di eseguire più operazioni parallele, sfruttando al meglio le risorse computazionali del calcolatore. Sebbene ciò sia vero, la programmazione concorrente nasconde spesso alcune difficoltà non banali, che vanno gestite opportunamente per evitare errori come deadlock o problemi di sincronizzazione.

In questa lezione vedremo le principali opzioni offerte da Python per programmare con i thread. A tale scopo, la lezione si concentrerà sull'uso del modulo threading, mentre il modulo _thread (successore di quello che su Python 2.x era il modulo thread) non sarà trattato in quanto considerato deprecato dalla community di Python.

Creazione ed avvio di un thread

La creazione di un thread con Python 3 necessita della definizione di una classe, che erediti dalla classe Thread. Quest'ultima è inclusa nel modulo threading, che va quindi importato. La classe che definiremo (rappresentante dunque il nostro thread) dovrà rispettare una precisa struttura: dovremo innanzitutto definire il metodo __init__, ma soprattutto dovremo sovrascrivere il metodo run.

Per capire meglio come procedere, vediamo un semplice esempio pratico:

from threading import Thread
import time
class IlMioThread (Thread):
   def __init__(self, nome, durata):
      Thread.__init__(self)
      self.nome = nome
      self.durata = durata
   def run(self):
      print ("Thread '" + self.name + "' avviato")
      time.sleep(self.durata)
      print ("Thread '" + self.name + "' terminato")

Abbiamo così definito un classe IlMioThread, che possiamo utilizzare per creare tutti i thread che vogliamo. Ogni thread di questo tipo sarà caratterizzato dalle operazioni definite nel metodo run, che in questo semplice esempio si limita a stampare una stringa all'inizio ed alla fine della sue esecuzione. Nel metodo __init__, inoltre, abbiamo specificato due parametri di inizializzazione (che poi sono utilizzati nel metodo run): tali parametri saranno specificati in fase di creazione del thread.

Vediamo ora come creare uno o più thread, a partire dalla precedente definizione della classe IlMioThread:

from random import randint
# Creazione dei thread
thread1 = IlMioThread("Thread#1", randint(1,100))
thread2 = IlMioThread("Thread#2", randint(1,100))
thread3 = IlMioThread("Thread#3", randint(1,100))
# Avvio dei thread
thread1.start()
thread2.start()
thread3.start()
# Join
thread1.join()
thread2.join()
thread3.join()
# Fine dello script
print("Fine")

In questo esempio, abbiamo creato tre thread, ognuno con le sue proprietà nome e durata (in accordo alla definizione del metodo __init__). Li abbiamo poi avviati mediante il metodo start, il quale si limita ad eseguire il contenuto del metodo run precedentemente definito. Si noti che il metodo start non è bloccante: quando esso viene eseguito, il controllo passa subito alla riga successiva, mentre il thread viene avviato in background. Per attendere che un thread termini, è necessario eseguire una operazione di join, come fatto nel codice precedente.

Sincronizzazione

Il modulo threading di Python include anche un semplice meccanismo di lock, che permette di implementare la sincronizzazione tra i thread. Un lock non è altro che un oggetto (tipicamente accessibile da più thread) di cui un thread deve "entrare in possesso" prima di poter procedere all'esecuzione di una sezione protetta di un programma. Tali lock sono creati eseguendo il metodo Lock(), definito nel summenzionato modulo threading.

Una volta ottenuto il lock, possiamo utilizzare due metodi che ci permettono di sincronizzare l'esecuzione di due (o più) thread: il metodo acquire per acquisire il controllo del lock, ed il metodo release per rilasciarlo. Il metodo acquire accetta un parametro opzionale che, se non specificato o impostato a True, forza il thread a sospendere la sua esecuzione finché il lock verrà rilasciato e potrà quindi essere acquisito. Se, invece, il metodo acquire viene eseguito con argomento pari a False, esso ritorna immediatamente un risultato booleano, che vale True se il lock è stato acquisito, oppure False in caso contrario.

L'esempio seguente mostra come utilizzare il meccanismo dei lock su Python:

import threading
import time.
from random import randint
# Definizione del lock
threadLock = threading.Lock()
class IlMioThread (threading.Thread):
   def __init__(self, nome, durata):
      threading.Thread.__init__(self)
      self.nome = nome
      self.durata = durata
   def run(self):
      print ("Thread '" + self.name + "' avviato")
      # Acquisizione del lock
      threadLock.acquire()
      time.sleep(self.durata)
      print ("Thread '" + self.name + "' terminato")
      # Rilascio del lock
      threadLock.release()
# Creazione dei thread
thread1 = IlMioThread("Thread#1", randint(1,100))
thread2 = IlMioThread("Thread#2", randint(1,100))
thread3 = IlMioThread("Thread#3", randint(1,100))
# Avvio dei thread
thread1.start()
thread2.start()
thread3.start()
# Join
thread1.join()
thread2.join()
thread3.join()
# Fine dello script
print("Fine")

Abbiamo modificato il codice precedente tramite l'uso dei lock, in modo che essi vengano eseguito in sequenza: il primo thread, infatti, acquisirà il lock e verrà eseguito mentre gli altri due thread rimarranno in attesa (poiché il metodo acquire è eseguito senza parametri). Al termine dell'esecuzione del primo thread, il secondo otterrà il lock e l'ultimo thread dovrà rimanere ancora in attesa, fino al termine dell'esecuzione del secondo.

Conclusioni

Come già anticipato, l'uso dei thread permette di realizzare applicazioni concorrenti anche molto complesse, che possono quindi essere difficili da gestire. La gestione corretta della sincronizzazione è fondamentale per il corretto funzionamento di un'applicazione, e può essere gestita sfruttando anche altre funzionalità messe a disposizione da Python. Per approfondire meglio, suggeriamo di fare riferimento alla documentazione ufficiale.

Ti consigliamo anche