Threads

I computer sarebbero molto meno utili se non potessimo fare più di una cosa alla volta. Ascoltare il nostro player audio preferito durante la lettura di un tutorial su Python nel mondo Geek, sarebbe impossibile.
Ma come si otteneva questo sui vecchi computer con un singolo core / una sola CPU? Ciò che succedeva, e ciò che accade ancora oggi è che non stiamo effettivamente eseguendo più processi contemporaneamente (si chiama processo un programma in esecuzione), ma che i processi si turnano e, data la velocità con cui si eseguono le istruzioni, si ha l’impressione che i compiti sono eseguiti in parallelo, come se avessimo un vero multitasking.
Cosa sono i processi e i thread?
Ogni volta che un processo diverso si esegue è necessario realizzare il cosiddetto cambio di contesto, durante il quale si salva lo stato del programma in esecuzione in memoria e si carica lo stato del programma che sta per entrare in esecuzione.
In Python possiamo creare nuovi processi attraverso la funzione os.fork, che esegue la chiamata al sistema fork, o attraverso altre funzioni più avanzate come popen2.popen2, in modo che il nostro programma possa eseguire diverse operazioni in parallelo.
Tuttavia, il cambio di contesto può essere relativamente lento, e le risorse necessarie per mantenere lo stato sono tante, quindi spesso è molto più efficiente utilizzare ciò che si conosce come threads (fili in esecuzione), o processi leggeri.
I threads sono simili nel concetto al processo: si tratta anche di codice d’esecuzione. Tuttavia, i threads si eseguono dentro di un processo e i threads del processo condividono le risorse tra loro, come la memoria, per esempio.
Il sistema operativo ha bisogno di meno risorse per creare e gestire i threads, e condividendo le risorse, il cambio di contesto è più veloce. Inoltre, poiché i threads condividono lo stesso spazio della memoria globale, è facile condividere le informazioni tra di loro: ogni variabile globale che abbiamo nel nostro programma è visto da tutti i threads.
Il GIL
L’esecuzione dei threads in Python è controllato dal GIL (Global Interpreter Lock) per far sì che si esegua un solo thread alla volta, indipendentemente dal numero di processori che ha la macchina. Questo consente che lo scrivere l’estensioni in C per Python sia molto più semplice, ma ha lo svantaggio di limitare molto le prestazioni; così nonostante tutto, in Python, a volte ci può interessare utilizzare più i processi che i threads, che non soffrono questa limitazione.
Ogni certo numero d’istruzioni di bytecode, la macchina virtuale ferma l’esecuzione del thread e sceglie un’altro tra coloro che sono in attesa.
Per impostazione predefinita il cambio di thread si realizza ogni 10 istruzioni di bytecode, ma può essere modificato utilizzando la funzione di sys.setcheckinterval. Il cambio di thread avviene anche quando il filo è messo a dormire con time.sleep o quando inizia un’operazione di input/output, le quali possono richiedere molto tempo per completarsi; e quindi, se non si realizza il cambio, avremo la CPU senza lavorare per troppo in attesa che l’operazione I/O finisca.
Per ridurre un poco l’effetto del GIL nel rendimento della nostra applicazione è conveniente chiamare l’interprete, con il flag -O, che genererà un bytecode ottimizzato con meno istruzioni, e quindi un minor numero di cambi di contesto. Potremmo anche chiedere di utlizzare i processi al posto dei thread, come si è visto prima, ad esempio utilizzando il modulo processing; scrivere del codice il cui rendimento è fondamentale in un’estensione in C o utilizzando IronPython o Jython, dove manca il GIL.
Threads in Python
Il lavoro con i thread viene eseguito dal modulo thread di Python. Questo modulo è facoltativo e dipende dalla piattaforma, e può essere necessario, anche se non è comune, ricompilare l’interprete per aggiungere il supporto dei threads.
Oltre a thread, abbiamo anche il modulo threading che si basa sul primo per fornire una API a livello superiore, più completa e object-oriented. Il modulo threading si basa leggermente nel modello thread di Java.
Il modulo threading contiene una classe Thread che dobbiamo estendere per creare i nostri propri thread. Il metodo run contiene il codice che vogliamo far eseguire al thread. Se vogliamo specificare il nostro proprio costruttore, questo si dovrà chiamare threading.Thread.__init__(self) per inizializzare l’oggetto in modo corretto.
			import threading
			class MioThread(threading.Thread):
			    def __init__(self, num):
			        threading.Thread.__init__(self)
			        self.num = num
			    def run(self):
			        print "Sono il thread", self.num
Quando creaiamo un’istanza della classe appena definita e chiamiamo il suo metodo start, il thread inizia ad eseguire il proprio codice. Il codice di thread principale e quello appena creato si eseguiranno in forma concorrente.
			print "Sono il thread principale"
			for i in range(0, 10):
			    t = MioThread(i)
			    t.start()
			    t.join()
Il metodo join si utilizza per bloccare il thread che esegue la chiamata fino a che finalizzi il thread su cui viene chiamato. In questo caso viene utilizzato per non fare terminare l’esecuzione del thread principale prima dei suoi figli (potrebbe accadere in alcune piattaforme la cessazione dei figli prima di finalizzare la sua esecuzione). Il metodo join può prendere come parametro un numero decimale che indica il numero massimo di secondi per l’attesa.
Se si tenta di chiamare il metodo start per un’istanza ch’è già in esecuzione, si ottiene un’eccezione.
Il modo consigliato per creare nuovi thread è quello di estendere la classe Thread, come abbiamo visto; anche s’è possibile creare un’istanza di Thread direttamente e indicare come parametri del costruttore una classe eseguibile (una classe con il metodo speciale __call__), o una funzione da eseguire e gli argomenti in una tupla (parametro args), o in un dizionario (parametro kwargs).
			import threading
			def mostra(num):
			    print "Sono il thread", num
			print "Sono il thread principale"
			for i in range(0, 10):
			    t = threading.Thread(target=mostra, args=(i, ))
			    t.start()
Oltre ai parametri target, args e kwargs possiamo passare al costruttore anche: un parametro name di tipo stringa con il nome che vogliamo che prenda il thread (il thread avrà sempre un nome predefinito, anche se non si specifica); un parametro verbose di tipo booleano per indicare al modulo di stampare i messaggi sullo stato dei threads per il debug; e un parametro group, che attualmente non supporta alcun valore, ma che in futuro potrà essere utilizzato per creare gruppi di threads e poter lavorare a livello di gruppo.
Per verificare se un thread è ancora in esecuzione, è possibile utilizzare il metodo isAlive. Possiamo anche assegnare un nome al thread e consultare il suo nome con i metodi setName e getName, rispettivamente.
Utilizzando la funzione threading.enumerate otteniamo una lista di oggetti Thread che sono in esecuzione, tra cui il thread principale (possiamo confrontare l’oggetto Thread con la variabile main_thread per verificare se si tratta del thread principale) e con threading.activeCount possiamo consultare il numero di threads in esecuzione.
Gli oggetti Thread contano anche con un metodo setDaemon che accetta un valore booleano che indica s’è un demone. L’utilità di questo è che se ci sono solo thread di tipo demoni in esecuzione tipo, l’applicazione terminerà automaticamente, finalizzando questi threads in modo sicuro.
Infine, abbiamo nel modulo threading una classe Timer che eredita da Thread e il cui utilizzo è quello di eseguire il codice del suo metodo run dopo un periodo di tempo, specificato come parametro nel suo costruttore. Include anche un metodo cancel per annullare l’esecuzione prima che raggiunga la fine del periodo di attesa.
Sincronizzazione
Uno dei maggiori problemi che dobbiamo affrontare quando si utilizzano i threads è la necessità di sincronizzare l’accesso a determinate risorse da parte dei threads. Tra i meccanismi di sincronizzazione che abbiamo a disposizione nel modulo threading ci sono i locks, i locks rientranti, i semafori, le condizioni e gli eventi.
I locks, detti anche mutex (di mutua esclusione), chiusure di esclusione mutua, serrature o lucchetti, sono oggetti con due possibili stati: acquisito o libero. Quando un thread acquisisce il lucchetto, gli altri thread per arrivano a questo punto più tardi e chiedono di acquisirlo verranno bloccati fino a quando il thread che lo ha acquisito rilasci il lucchetto, momento in cui un altro thread potrà entrare.
Il lucchetto è rappresentato dalla classe Lock. Per acquisire il lucchetto si utilizza il metodo acquire dell’oggetto, al quale si può passare un valore booleano per indicare se vogliamo attende il rilascio (True) o meno (False). Se indichiamo che non vogliamo aspettare, il metodo restituirà True o False a seconda se si è acquisito o no il lucchetto, rispettivamente. Per impostazione predefinita, se non è indicato, il thread si blocca a tempo indeterminato.
Per rilasciare il lucchetto una volta che abbiamo finito di eseguire il blocco di codice dove potrebbe prodursi un problema di concorrenza, si utilizza il metodo release.
			lista = []
			lock = threading.Lock()
			
			def aggiungere(obj):
			    lock.acquire()
			    lista.append(obj)
			    lock.release()
			def prendere():
			    lock.acquire()
			    obj = lista.pop()
			    lock.release()
			    return obj
La classe RLock funziona in modo simile a Lock, ma in questo caso il lucchetto può essere acquisito dallo stesso thread più volte, e non sarà rilasciato fino a quando il thread chiami a release le volte che ha chiamato acquire. Come in Lock, e come in tutte le primitive di sincronizzazione che vedremo in seguito, è possibile indicare a acquire se vogliamo che si blocchi o no.
I semafori sono un’altra classe di lucchetti. La classe corrispondente, Semaphore, ha anche i metodi acquire e release, ma si differenza di un Lock normale perché il costruttore di Semaphore può prendere un parametro facoltativo un numero intero che indica il numero massimo di thread che può accedere alla sezione codice critico. Se non è indicato consente l’accesso ad un singolo thread.
Quando un thread chiama ad acquire, la variabile che indica il numero di thread che possono acquisire il semaforo diminuisce di 1, perché abbiamo permesso di entrare nella sezione del codice critico un filo in più. Quando un filo chiama a release, la variabile viene incrementata di 1.
Quando il valore di questa variabile del semaforo è 0, chiamare ad acquire produrrà un blocco sul thread che ha effettuato la richiesta, in attesa che qualche altro thread chiami a release per liberare il suo posto.
È importante sottolineare che il valore iniziale della variabile che si passa al costruttore non è il limite massimo; varie chiamate a release possono rendere il valore della variabile superiore al suo valore originale. Se questo non è ciò che vogliamo, possiamo usare la classe BoundedSemaphore, nel qual caso, ora sarebbe considerato un errore chiamare a release troppe volte, e genererebbe un’eccezione di tipo ValueError al superare il valore iniziale.
Potremmo usare i semafori, per esempio, in un piccolo programma in cui più thread scaricano dati da un URL, in modo da poter limitare il numero di collegamenti da effettuare al sito per non bombardare il sito con centinaia di richieste simultanee.
			semaforo = threading.Semaphore(4)
			
			def scaricare(url):
			    semaforo.acquire()
			    urllib.urlretrieve(url)
			    semaforo.release()
Le condizioni (classe Condition) sono utili per fare entrare ai thread nella sezione critica se si da una certa condizione o evento. Per questo utilizzano un Lock passato come parametro, o creano un oggetto RLock automaticamente se non viene passato nessun parametro al costruttore.
Sono particolarmente adatti per il classico problema produttore/consumatore. La classe ha i metodi acquire e release, che chiameranno i metodi corrispondenti del lucchetto associato. Abbiamo anche i metodi wait, notify e notifyAll.
Il metodo wait deve essere chiamato dopo aver acquisito il lucchetto con acquire. Questo metodo rilascia il lucchetto e blocca il thread fino a quando una chiamata a notify o a notifyAll in un altro thread dove si è compiuta la condizione attesa. Il thread che informa agli altri che la condizione è verificata, chiama inoltre ad acquire prima di chiamare a notify o a notifyAll.
Quando si chiama notify, si informa dell’evento a un singolo thread, e quindi si sveglia un singolo thread. Quando si chiama notifyAll si svegliano tutti i thread che erano in attesa della condizione.
Sia il thread che segnala come quelli che son modificati debbono terminare liberando il lucchetto con release.
			lista = []
			cond = threading.Condition()
			
			def consumare():
			    cond.acquire()
			    cond.wait()
			    obj = lista.pop()
			    cond.release()
			    return obj
			
			def produrre(obj):
			    cond.acquire()
			    lista.append(obj)
			    cond.notify()
			    cond.release()
Gli eventi, implementati con la classe Event sono un wrapper al di sopra di Condition e servono principalmente per coordinare i threads con dei segni che indicano se un evento si è verificato. Gli eventi ci astraggono dal fatto che stiamo usando un Lock al di sotto, per cui non hanno i metodi acquire e release.
Il thread che deve attendere l’evento, chiama al metodo wait e si blocca, eventualmente passando come parametro un numero decimale che indica il numero massimo di secondi di attesa. Un altro thread, quando si verifica l’evento, invia un segnale ai thread bloccati in attesa dell’evento utilizzando il metodo set. I threads che erano in attesa si sbloccato dopo aver ricevuto il segnale. Il flag che determina se si è prodotto l’evento può essere re-impostata su False utilizzando clear.
Come si vede gli eventi sono molto simili alle condizioni, tranne che si sbloccano tutti i thread erano in attesa dell’evento e di non dover chiamare ad acquire e release.
			import threading, time
			
			class MioThread(threading.Thread):
			    def __init__(self, evento):
			        threading.Thread.__init__(self)
			        self.evento = evento
			
			    def run(self):
			        print self.getName(), "attendendo l'evento"
			        self.evento.wait()
			        print self.getName(), "termina l'attesa"
			
			evento = threading.Event()
			t1 = MioThread(evento)
			t1.start()
			t2 = MioThread(evento)
			t2.start()
			
			# Attendere un poco
			time.sleep(5)
			evento.set()
Infine, un piccolo extra. Se conoscete Java magari vi rendete conto che manca una parola chiave syncronized per fare in modo che solo un thread alla volta possa accedere al metodo in cui viene utilizzato. Una costruzione comune è quello di utilizzare un decoratore per implementare questa funzionalità utilizzando un Lock. Sarebbe qualcosa di simile:
			def synchronized(lock):
			    def dec(f):
			        def func_dec(*args, **kwargs):
			            lock.acquire()
			            try:
			                return f(*args, **kwargs)
			            finally:
			                lock.release()
			        return func_dec
			    return dec
			
			class MioThread(threading.Thread):
			    @synchronized(mi_lock)
			    def run(self):
			        print "metodo sincronizzato"
Dati globali indipendenti
Come già commentato i thread condividono le variabili globali. Tuttavia ci possono essere situazioni in cui vogliamo utilizzare le variabili globali, ma che queste variabili si comportino come se fossero locali ad un thread. Cioè, che ogni thread abbiano valori diversi e indipendenti, e che le modifiche in un particolare thread sul valore non si riflette nelle copie degli altri thread.
Per ottenere questo comportamento può essere utilizzato classe threading.local, che crea un archivio di dati locale. Prima di tutto dobbiamo creare un’istanza della classe o una sottoclasse, per memorizzare e recuperare i valori attraverso i parametri della classe.
			dati_locali = threading.local()
			dati_locali.mia_var = "Ciao"
			print dati_locali.mia_var
Considariamo il seguente codice, per esempio. Per il thread principale l’oggetto ha un attributo locale var, e quindi il print stampa suo valore senza problemi. Tuttavia, per il thread t quell’attributo non esiste, e genera quindi un’eccezione.
			locale = threading.local()
			def f():
			    print locale.var
			locale.var = "Ciao"
			t = threading.Thread(target=f)
			print locale.var
			t.start()
			t.join()
Condivisione delle informazioni
Per condividere le informazioni facilmente tra i thread possiamo usare la classe Queue.Queue che implementa una coda (struttura dati tpo FIFO) con supporto multithreading. Questa classe utilizza le primitive di threading per evitare di dover sincronizzare l’accesso ai dati da noi stessi.
Il costruttore di Queue accetta un parametro opzionale che indica la dimensione massima della coda. Se non si specifica un valore non ci sarà alcun limite per la sua dimensione.
Per aggiungere un elemento alla coda si utilizza il metodo put(item); per ottenere l’elemento successivo, get(). Entrambi i metodi hanno un parametro booleano opzionale block che indica se dobbiamo aspettare finché non ci sia qualche elemento in coda per tornare o fino a quando la coda non sia più piena per introdurlo.
Vi è anche un parametro opzionale timeout che indica, in secondi, il tempo massimo di attesa. Se il timeout finisce senza eseguire l’operazione perché la coda era piena o vuota, o se block era False, viene generata un’eccezione di tipo Queue.Full o Queue.Empty, rispettivamente.
Con qsize otteniamo la dimensione della coda e con empty() e full() possiamo verificare se è vuoto o pieno.
			q = Queue.Queue()
			class MioThread(threading.Thread):
			    def __init__(self, q):
			        self.q = q
			        threading.Thread.__init__(self)
			
			    def run(self):
			        while True:
			            try:
			                obj = q.get(False)
			            except Queue.Empty:
			                print "Fine"
			                break
			            print obj
			
			for i in range(10):
			    q.put(i)
			
			t = MioThread(q)
			t.start()
			t.join()


Similari
Overloading di metodi in Java
12% Java
Un metodo overload viene utilizzato per riutilizzare il nome di un metodo ma con argomenti diversi, opzionalmente con un differente tipo di ritorno. [expand title=”Regole per overload” startwrap=”” endwrap=”” excerpt=”⤽” s…
Modi di fare e di non fare in Python
11% Python
Questo documento può essere considerato un compagno del tutorial di Python. Viene illustrato come utilizzare Python, e quasi ancora più importante, come non usare Python. [expand title=”Costrutti del linguaggio che non dov…
redirect 301 usando mod_alias
9% Server
mod_alias è fondamentalmente la versione più semplice di mod_rewrite. Non può fare le cose che fa mod_rewrite, ad esempio modificare la stringa di query. Per eseguire reindirizzamenti nel server web Apache è possibile di u…
Installare Python e Django su Windows
8% Django
Quando ci riferiamo allo sviluppo web con Python, la prima cosa che viene in mente è usare un qualche framework. Il più famoso e utilizzato da tutti è il Django, ma non è l’unico. Ci sono Pylons, Grok, TurboGears e Zope: t…
Metodi magici e costanti predefinite in PHP
8% Php
PHP fornisce un insieme di costanti predefinite e metodi magici per i nostri programmi. A differenza delle normali costanti i quali si impostano con define(), il valore delle costanti predefinite o speciali dipendono da do…