Ingresso multithread Python con Esempio: Impara GIL in Python

Il linguaggio di programmazione Python consente di utilizzare il multiprocessing o il multithreading. In questo tutorial, imparerai come scrivere applicazioni multithread in Python.

Che cos'รจ un filo?

Un thread รจ un'unitร  di esecuzione sulla programmazione simultanea. Il multithreading รจ una tecnica che consente a una CPU di eseguire piรน attivitร  di un processo contemporaneamente. Questi thread possono essere eseguiti individualmente condividendo le risorse del processo.

Cos'รจ un processo?

Un processo รจ fondamentalmente il programma in esecuzione. Quando avvii un'applicazione sul tuo computer (come un browser o un editor di testo), il sistema operativo crea un file

In cosa consiste il multithreading Python?

Ingresso multithread Python la programmazione รจ una tecnica ben nota in cui piรน thread in un processo condividono il proprio spazio dati con il thread principale, rendendo la condivisione delle informazioni e la comunicazione all'interno dei thread semplice ed efficiente. I thread sono piรน leggeri dei processi. I multi thread possono essere eseguiti individualmente condividendo le risorse del processo. Lo scopo del multithreading รจ eseguire piรน attivitร  e celle funzionali contemporaneamente.

Cos'รจ la multielaborazione?

multiprocessing consente di eseguire piรน processi non correlati contemporaneamente. Questi processi non condividono le loro risorse e comunicano tramite IPC.

Python Multithreading e multiprocessing

Per comprendere processi e thread, considera questo scenario: un file .exe sul tuo computer รจ un programma. Quando lo apri, il sistema operativo lo carica in memoria e la CPU lo esegue. L'istanza del programma che รจ ora in esecuzione รจ chiamata processo.

Ogni processo avrร  2 componenti fondamentali:

  • Il codice
  • I dati

Ora, un processo puรฒ contenere una o piรน sottoparti chiamate thread. Dipende dall'architettura del sistema operativo. รˆ possibile pensare a un thread come a una sezione del processo che puรฒ essere eseguita separatamente dal sistema operativo.

In altre parole, รจ un flusso di istruzioni che puรฒ essere eseguito indipendentemente dal sistema operativo. I thread all'interno di un singolo processo condividono i dati di quel processo e sono progettati per lavorare insieme per facilitare il parallelismo.

Perchรฉ utilizzare il multithreading?

Il multithreading consente di suddividere un'applicazione in piรน sotto-attivitร  ed eseguirle simultaneamente. Se si utilizza il multithreading correttamente, la velocitร , le prestazioni e il rendering dell'applicazione possono essere tutti migliorati.

Python Multithreading

Python supporta costrutti sia per il multiprocessing che per il multithreading. In questo tutorial ti concentrerai principalmente sull'implementazione multithreaded applicazioni con python. Ci sono due moduli principali che possono essere utilizzati per gestire i thread in Python:

  1. Migliori filo modulo, e
  2. Migliori threading modulo

Tuttavia, in Python esiste anche qualcosa chiamato global interpreter lock (GIL). Non consente molti miglioramenti in termini di prestazioni e potrebbe anche ridurre le prestazioni di alcune applicazioni multithread. Imparerai tutto al riguardo nelle prossime sezioni di questo tutorial.

I moduli Thread e Threading

I due moduli che imparerai in questo tutorial sono il modulo filo e modulo di filettatura.

Tuttavia, il modulo thread รจ stato da tempo deprecato. A partire da Python 3, รจ stato designato come obsoleto ed รจ accessibile solo come __filo per la retrocompatibilitร .

Dovresti usare il livello piรน alto threading modulo per le applicazioni che si intende distribuire. Il modulo thread รจ stato trattato qui solo per scopi didattici.

Il modulo Discussione

La sintassi per creare un nuovo thread utilizzando questo modulo รจ la seguente:

thread.start_new_thread(function_name, arguments)

Bene, ora hai trattato la teoria di base per iniziare a programmare. Quindi, apri il tuo IDLE oppure un blocco note e digitare quanto segue:

import time
import _thread

def thread_test(name, wait):
   i = 0
   while i <= 3:
      time.sleep(wait)
      print("Running %s\n" %name)
      i = i + 1

   print("%s has finished execution" %name)

if __name__ == "__main__":
    
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

Salva il file e premi F5 per eseguire il programma. Se tutto รจ stato fatto correttamente, questo รจ l'output che dovresti vedere:

Il modulo Discussione

Imparerai di piรน sulle condizioni di gara e su come gestirle nelle prossime sezioni

Il modulo Discussione

SPIEGAZIONE DEL CODICE

  1. Queste istruzioni importano il tempo e il modulo thread utilizzati per gestire l'esecuzione e il ritardo del file Python thread.
  2. Qui hai definito una funzione chiamata test_thread, che sarร  chiamato dal start_new_thread metodo. La funzione esegue un ciclo while per quattro iterazioni e stampa il nome del thread che l'ha chiamata. Una volta completata l'iterazione, stampa un messaggio che informa che il thread ha terminato l'esecuzione.
  3. Questa รจ la sezione principale del tuo programma. Qui, chiami semplicemente il start_new_thread metodo con il thread_test funzione come argomento. Questo creerร  un nuovo thread per la funzione che passi come argomento e inizierร  ad eseguirla. Tieni presente che puoi sostituire questo (thread_test) con qualsiasi altra funzione che desideri eseguire come thread.

Il modulo di threading

Questo modulo รจ l'implementazione di alto livello del threading in Python e lo standard de facto per la gestione delle applicazioni multithread. Fornisce una vasta gamma di funzionalitร  rispetto al modulo thread.

Struttura del modulo Threading
Struttura del modulo Threading

Ecco un elenco di alcune funzioni utili definite in questo modulo:

Nome della funzione Descrizione
conteggioattivo() Restituisce il conteggio di Filo oggetti ancora vivi
thread corrente() Restituisce l'oggetto corrente della classe Thread.
enumerare() Elenca tutti gli oggetti Thread attivi.
รจDaemon() Restituisce vero se il thread รจ un demone.
รจ vivo() Restituisce vero se il thread รจ ancora vivo.
Metodi della classe thread
inizio() Avvia l'attivitร  di un thread. Deve essere chiamato solo una volta per ciascun thread perchรฉ genererร  un errore di runtime se chiamato piรน volte.
correre() Questo metodo denota l'attivitร  di un thread e puรฒ essere sovrascritto da una classe che estende la classe Thread.
aderire() Blocca l'esecuzione di altro codice finchรฉ il thread su cui รจ stato chiamato il metodo join() non viene terminato.

Storia: la classe Thread

Prima di iniziare a codificare programmi multithread utilizzando il modulo threading, รจ fondamentale comprendere la classe Thread. La classe thread รจ la classe primaria che definisce il modello e le operazioni di un thread in Python.

Il modo piรน comune per creare un'applicazione Python multithread รจ dichiarare una classe che estende la classe Thread e sovrascrive il suo metodo run().

La classe Thread, in sintesi, indica una sequenza di codice che viene eseguita in un file separato filo di controllo.

Quindi, quando scrivi un'app multithread, dovrai fare quanto segue:

  1. definire una classe che estende la classe Thread
  2. Sostituisci il __init__ costruttore
  3. Sostituisci il correre() metodo

Una volta creato un oggetto thread, il file inizio() puรฒ essere utilizzato per iniziare l'esecuzione di questa attivitร  e il aderire() Il metodo puรฒ essere utilizzato per bloccare tutto il resto del codice fino al termine dell'attivitร  corrente.

Ora proviamo a utilizzare il modulo threading per implementare l'esempio precedente. Ancora una volta, accendi il tuo IDLE e digita quanto segue:

import time
import threading

class threadtester (threading.Thread):
    def __init__(self, id, name, i):
       threading.Thread.__init__(self)
       self.id = id
       self.name = name
       self.i = i
       
    def run(self):
       thread_test(self.name, self.i, 5)
       print ("%s has finished execution " %self.name)

def thread_test(name, wait, i):

    while i:
       time.sleep(wait)
       print ("Running %s \n" %name)
       i = i - 1

if __name__=="__main__":
    thread1 = threadtester(1, "First Thread", 1)
    thread2 = threadtester(2, "Second Thread", 2)
    thread3 = threadtester(3, "Third Thread", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

Questo sarร  l'output quando esegui il codice sopra:

Storia: la classe Thread

SPIEGAZIONE DEL CODICE

Storia: la classe Thread

  1. Questa parte รจ la stessa del nostro esempio precedente. Qui, importi il โ€‹โ€‹modulo time e thread che vengono utilizzati per gestire l'esecuzione e i ritardi del Python thread.
  2. In questo momento stai creando una classe chiamata threadtester, che eredita o estende il file Filo classe del modulo threading. Questo รจ uno dei modi piรน comuni per creare thread in Python. Tuttavia, dovresti sovrascrivere solo il costruttore e il file correre() metodo nella tua app. Come puoi vedere nell'esempio di codice sopra, il file __init__ il metodo (costruttore) รจ stato sovrascritto. Allo stesso modo, hai anche sovrascritto il file correre() metodo. Contiene il codice che vuoi eseguire all'interno di un thread. In questo esempio, hai chiamato la funzione thread_test().
  3. Questo รจ il metodo thread_test() che prende il valore di i come argomento, lo diminuisce di 1 ad ogni iterazione e scorre il resto del codice finchรฉ i diventa 0. In ogni iterazione, stampa il nome del thread attualmente in esecuzione e dorme per wait secondi (che viene anche preso come argomento ).
  4. thread1 = threadtester(1, โ€œFirst Threadโ€, 1) Qui stiamo creando un thread e passando i tre parametri che abbiamo dichiarato in __init__. Il primo parametro รจ l'id del thread, il secondo parametro รจ il nome del thread e il terzo parametro รจ il contatore, che determina quante volte deve essere eseguito il ciclo while.
  5. thread2.start() Il metodo start viene utilizzato per avviare l'esecuzione di un thread. Internamente, la funzione start() chiama il metodo run() della tua classe.
  6. thread3.join() Il metodo join() blocca l'esecuzione di altro codice e attende fino al termine del thread su cui รจ stato chiamato.

Come giร  saprai, i thread che si trovano nello stesso processo hanno accesso alla memoria e ai dati di quel processo. Di conseguenza, se piรน thread provano a modificare o ad accedere ai dati contemporaneamente, potrebbero insinuarsi degli errori.

Nella sezione successiva verranno visualizzati i diversi tipi di complicazioni che possono verificarsi quando i thread accedono ai dati e alla sezione critica senza verificare le transazioni di accesso esistenti.

Stalli e condizioni di gara

Prima di approfondire l'argomento dei deadlock e delle condizioni di gara, sarร  utile comprendere alcune definizioni di base relative alla programmazione concorrente:

  • Sezione criticaรˆ un frammento di codice che accede o modifica variabili condivise e deve essere eseguito come una transazione atomica.
  • Cambio di contestoรˆ il processo seguito da una CPU per memorizzare lo stato di un thread prima di passare da un'attivitร  all'altra, in modo che possa essere ripresa dallo stesso punto in un secondo momento.

Deadlock

Deadlock sono il problema piรน temuto che gli sviluppatori affrontano quando scrivono applicazioni concorrente/multithread in python. Il modo migliore per comprendere i deadlock รจ usare il classico esempio di problema di informatica noto come Ristoranti PhiloProblema di Sopher.

Il problema per i filosofi della tavola รจ il seguente:

Cinque filosofi sono seduti su un tavolo rotondo con cinque piatti di spaghetti (un tipo di pasta) e cinque forchette, come mostrato nel diagramma.

Ristoranti PhiloProblema di Sopher

Ristoranti PhiloProblema di Sopher

In ogni momento, un filosofo deve stare mangiando o pensando.

Inoltre, un filosofo deve prendere le due forchette adiacenti a lui (cioรจ, la forchetta sinistra e quella destra) prima di poter mangiare gli spaghetti. Il problema dello stallo si verifica quando tutti e cinque i filosofi prendono contemporaneamente la loro forchetta destra.

Poichรฉ ognuno dei filosofi ha una forchetta, aspetteranno tutti che gli altri la appoggino. Di conseguenza, nessuno di loro potrร  mangiare gli spaghetti.

Allo stesso modo, in un sistema concorrente, si verifica un deadlock quando diversi thread o processi (filosofi) tentano di acquisire le risorse di sistema condivise (fork) contemporaneamente. Di conseguenza, nessuno dei processi ha la possibilitร  di essere eseguito poichรฉ sono in attesa di un'altra risorsa detenuta da un altro processo.

Condizioni di regata

Una race condition รจ uno stato indesiderato di un programma che si verifica quando un sistema esegue due o piรน operazioni contemporaneamente. Ad esempio, considera questo semplice ciclo for:

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

Se crei n numero di thread che eseguono questo codice contemporaneamente, non รจ possibile determinare il valore di i (che รจ condiviso dai thread) quando il programma termina l'esecuzione. Questo perchรฉ in un ambiente multithreading reale, i thread possono sovrapporsi e il valore di i che รจ stato recuperato e modificato da un thread puรฒ cambiare nel frattempo quando un altro thread vi accede.

Queste sono le due principali classi di problemi che possono verificarsi in un'applicazione python multithread o distribuita. Nella prossima sezione, imparerai come superare questo problema sincronizzando i thread.

Syncfili cronici

Per gestire condizioni di gara, deadlock e altri problemi basati sui thread, il modulo di threading fornisce bloccare oggetto. L'idea รจ che quando un thread vuole accedere a una risorsa specifica, acquisisce un blocco per quella risorsa. Una volta che un thread blocca una risorsa particolare, nessun altro thread puรฒ accedervi finchรฉ il blocco non viene rilasciato. Di conseguenza, le modifiche alla risorsa saranno atomiche e le condizioni di gara saranno evitate.

Un blocco รจ una primitiva di sincronizzazione di basso livello implementata da __filo modulo. In qualsiasi momento, una serratura puรฒ trovarsi in uno dei 2 stati: bloccato or sbloccato. Supporta due metodi:

  1. acquisire()Quando lo stato di blocco รจ sbloccato, la chiamata al metodo acquire() modificherร  lo stato in bloccato e restituirร . Tuttavia, se lo stato รจ bloccato, la chiamata ad acquire() viene bloccata finchรฉ il metodo release() non viene chiamato da qualche altro thread.
  2. pubblicazione()Il metodo release() viene utilizzato per impostare lo stato su sbloccato, ovvero per rilasciare un blocco. Puรฒ essere chiamato da qualsiasi thread, non necessariamente da quello che ha acquisito il lock.

Ecco un esempio di utilizzo dei blocchi nelle tue app. Accendi il tuo IDLE e digita quanto segue:

import threading
lock = threading.Lock()

def first_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the first funcion')
        lock.release()

def second_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the second funcion')
        lock.release()

if __name__=="__main__":
    thread_one = threading.Thread(target=first_function)
    thread_two = threading.Thread(target=second_function)

    thread_one.start()
    thread_two.start()

    thread_one.join()
    thread_two.join()

Ora premi F5. Dovresti vedere un output come questo:

SyncDiscussioni croniche

SPIEGAZIONE DEL CODICE

SyncDiscussioni croniche

  1. In questo caso stai semplicemente creando una nuova serratura chiamando il file threading.Lock() funzione di fabbrica. Internamente, Lock() restituisce un'istanza della classe Lock concreta piรน efficace gestita dalla piattaforma.
  2. Nella prima istruzione acquisisci il blocco chiamando il metodo acquire(). Una volta concesso il blocco, si stampa โ€œblocco acquisitoโ€ alla consolle. Una volta terminata l'esecuzione di tutto il codice che desideri venga eseguito dal thread, rilasci il blocco chiamando il metodo release().

La teoria va bene, ma come fai a sapere che la serratura ha funzionato davvero? Se guardi l'output, vedrai che ciascuna delle istruzioni print stampa esattamente una riga alla volta. Ricordiamo che, in un esempio precedente, gli output di print erano casuali perchรฉ piรน thread accedevano al metodo print() contemporaneamente. In questo caso la funzione di stampa viene richiamata solo dopo l'acquisizione del lock. Pertanto, gli output vengono visualizzati uno alla volta e riga per riga.

Oltre ai blocchi, Python supporta anche altri meccanismi per gestire la sincronizzazione dei thread, come elencato di seguito:

  1. RLocks
  2. Semaphores
  3. Condizioni
  4. Eventi, e
  5. Barriere

Blocco globale interprete (e come gestirlo)

Prima di entrare nei dettagli del GIL di Python, definiamo alcuni termini che saranno utili per comprendere la prossima sezione:

  1. Codice associato alla CPU: si riferisce a qualsiasi parte di codice che verrร  eseguita direttamente dalla CPU.
  2. Codice associato a I/O: puรฒ essere qualsiasi codice che accede al file system tramite il sistema operativo
  3. CPython: รจ il riferimento implementazione of Python e puรฒ essere descritto come l'interprete scritto in C e Python (linguaggio di programmazione).

In cosa consiste GIL Python?

Blocco globale dell'interprete (GIL) in Python รจ un blocco di processo o un mutex utilizzato durante la gestione dei processi. Si assicura che un thread alla volta possa accedere a una particolare risorsa e impedisce anche l'uso simultaneo di oggetti e bytecode. Ciรฒ avvantaggia i programmi a thread singolo in un aumento delle prestazioni. GIL in Python รจ molto semplice e facile da implementare.

รˆ possibile utilizzare un blocco per assicurarsi che solo un thread abbia accesso a una particolare risorsa in un dato momento.

Una delle caratteristiche di Python รจ che utilizza un blocco globale su ogni processo dell'interprete, il che significa che ogni processo tratta l'interprete Python stesso come una risorsa.

Ad esempio, supponiamo di aver scritto un programma Python che utilizza due thread per eseguire entrambe le operazioni CPU e 'I/O'. Quando esegui questo programma, ecco cosa succede:

  1. L'interprete Python crea un nuovo processo e genera i thread
  2. Quando il thread-1 inizia a funzionare, acquisirร  prima il GIL e lo bloccherร .
  3. Se il thread-2 vuole essere eseguito adesso, dovrร  attendere il rilascio del GIL anche se un altro processore รจ libero.
  4. Supponiamo ora che il thread-1 sia in attesa di un'operazione di I/O. A questo punto, rilascerร  il GIL e il thread-2 lo acquisirร .
  5. Dopo aver completato le operazioni di I/O, se il thread-1 vuole essere eseguito adesso, dovrร  nuovamente attendere che il GIL venga rilasciato dal thread-2.

Per questo motivo, solo un thread alla volta puรฒ accedere all'interprete, il che significa che ci sarร  un solo thread che esegue il codice Python in un dato momento.

Questo va bene in un processore single-core perchรฉ utilizzerebbe il time slicing (vedere la prima sezione di questo tutorial) per gestire i thread. Tuttavia, nel caso di processori multi-core, una funzione legata alla CPU eseguita su piรน thread avrร  un impatto considerevole sull'efficienza del programma poichรฉ in realtร  non utilizzerร  tutti i core disponibili contemporaneamente.

Perchรฉ era necessario il GIL?

Il CPython garbage collector utilizza una tecnica di gestione della memoria efficiente nota come conteggio dei riferimenti. Ecco come funziona: ogni oggetto in python ha un conteggio dei riferimenti, che aumenta quando viene assegnato a un nuovo nome di variabile o aggiunto a un contenitore (come tuple, elenchi, ecc.). Allo stesso modo, il conteggio dei riferimenti diminuisce quando il riferimento esce dall'ambito o quando viene chiamata l'istruzione del. Quando il conteggio dei riferimenti di un oggetto raggiunge 0, viene sottoposto a garbage collection e la memoria assegnata viene liberata.

Ma il problema รจ che la variabile di conteggio dei riferimenti รจ soggetta a condizioni di gara come qualsiasi altra variabile globale. Per risolvere questo problema, gli sviluppatori di Python hanno deciso di usare il blocco dell'interprete globale. L'altra opzione era aggiungere un blocco a ogni oggetto, il che avrebbe causato deadlock e un sovraccarico maggiore dalle chiamate acquire() e release().

Pertanto, GIL rappresenta una restrizione significativa per i programmi Python multithread che eseguono operazioni pesanti legate alla CPU (rendendoli di fatto a thread singolo). Se desideri utilizzare piรน core CPU nella tua applicazione, utilizza il file multiprocessing modulo invece.

Sintesi

  • Python supporta 2 moduli per il multithreading:
    1. __filo modulo: fornisce un'implementazione di basso livello per il threading ed รจ obsoleto.
    2. modulo di filettatura: Fornisce un'implementazione di alto livello per il multithreading ed รจ lo standard attuale.
  • Per creare un thread utilizzando il modulo di threading, รจ necessario effettuare le seguenti operazioni:
    1. Crea una classe che estende il file Filo classe.
    2. Sostituisci il suo costruttore (__init__).
    3. Sostituiscilo correre() metodo.
    4. Crea un oggetto di questa classe.
  • Un thread puรฒ essere eseguito chiamando il metodo inizio() metodo.
  • Migliori aderire() Il metodo puรฒ essere utilizzato per bloccare altri thread finchรฉ questo thread (quello su cui รจ stato chiamato il join) non termina l'esecuzione.
  • Una condizione di competizione si verifica quando piรน thread accedono o modificano una risorsa condivisa contemporaneamente.
  • Puรฒ essere evitato da Syncfili cronici.
  • Python supporta 6 modi per sincronizzare i thread:
    1. Serrature
    2. RLocks
    3. Semaphores
    4. Condizioni
    5. Eventi, e
    6. Barriere
  • I blocchi consentono solo a un particolare thread che ha acquisito il blocco di entrare nella sezione critica.
  • Un blocco ha 2 metodi principali:
    1. acquisire(): Imposta lo stato di blocco su bloccato. Se richiamato su un oggetto bloccato, si blocca finchรฉ la risorsa non รจ libera.
    2. pubblicazione(): Imposta lo stato di blocco su sbloccato e ritorna. Se chiamato su un oggetto sbloccato, restituisce false.
  • Il blocco globale dell'interprete รจ un meccanismo attraverso il quale solo 1 CPython il processo dell'interprete puรฒ essere eseguito alla volta.
  • รˆ stato utilizzato per facilitare la funzionalitร  di conteggio dei riferimenti di CPythonil netturbino di s.
  • Per rendere Python per le app con operazioni che impegnano molto la CPU, dovresti usare il modulo multiprocessing.

Riassumi questo post con: