Domanda

Se stai facendo affidamento su un'implementazione di Python che ha un Global Interpreter Lock (ovvero CPython) e stai scrivendo codice multithread, hai davvero bisogno di blocchi?

Se il GIL non consente l'esecuzione di più istruzioni in parallelo, i dati condivisi non sarebbero necessari per la protezione?

scusate se questa è una domanda stupida, ma è qualcosa che mi sono sempre chiesto di Python su macchine multiprocessore / core.

la stessa cosa si applicherebbe a qualsiasi altra implementazione linguistica che abbia un GIL.

È stato utile?

Soluzione

Avrai comunque bisogno di blocchi se condividi lo stato tra i thread. Il GIL protegge solo l'interprete internamente. Puoi ancora avere aggiornamenti incoerenti nel tuo codice.

Ad esempio:

#!/usr/bin/env python
import threading

shared_balance = 0

class Deposit(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance += 100
            shared_balance = balance

class Withdraw(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance -= 100
            shared_balance = balance

threads = [Deposit(), Withdraw()]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print shared_balance

Qui, il tuo codice può essere interrotto tra la lettura dello stato condiviso ( balance = shared_balance ) e la scrittura del risultato modificato ( shared_balance = balance ), causando un aggiornamento perso . Il risultato è un valore casuale per lo stato condiviso.

Per rendere coerenti gli aggiornamenti, i metodi di esecuzione dovrebbero bloccare lo stato condiviso attorno alle sezioni read-edit-write (all'interno dei loop) o avere un modo per rilevare quando lo stato condiviso è cambiato da quando è stato letto .

Altri suggerimenti

No - GIL protegge solo gli interni di Python da più thread che ne alterano lo stato. Questo è un livello di blocco molto basso, sufficiente solo per mantenere le strutture di Python in uno stato coerente. Non copre il blocco del livello applicazione che dovrai fare per coprire la sicurezza del thread nel tuo codice.

L'essenza del blocco è garantire che un particolare blocco di codice sia eseguito da un solo thread. Il GIL impone questo per blocchi della dimensione di un singolo bytecode, ma di solito si desidera che il blocco si estenda su un blocco di codice più grande di questo.

Aggiunta alla discussione:

Poiché esiste il GIL, alcune operazioni sono atomiche in Python e non richiedono un blocco.

http : //www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

Come indicato dalle altre risposte, tuttavia, ancora è necessario utilizzare i blocchi ogni volta che la logica dell'applicazione li richiede (ad esempio in un problema produttore / consumatore).

Il Global Interpreter Lock impedisce ai thread di accedere contemporaneamente all ' interprete (quindi CPython usa sempre un solo core). Tuttavia, a quanto ho capito, i thread sono ancora interrotti e pianificati preventivamente , il che significa che hai ancora bisogno di blocchi sulle strutture di dati condivisi, per evitare che i tuoi thread si calpestino l'uno sull'altro.

La risposta che ho incontrato più volte è che il multithreading in Python raramente vale il sovraccarico, per questo motivo. Ho sentito parlare bene del PyProcessing , che rende l'esecuzione di più processi come " semplice " come multithreading, con strutture di dati condivise, code, ecc. (PyProcessing sarà introdotto nella libreria standard del prossimo Python 2.6 come modulo multiprocessing . Questo ti porta in giro per GIL, poiché ogni processo ha il suo interprete.

Questo post descrive il GIL a un livello abbastanza alto:

Di particolare interesse sono queste citazioni:

  

Ogni dieci istruzioni (impostazione predefinita   può essere modificato), il core rilascia il file   GIL per il thread corrente. A quel   punto, il sistema operativo sceglie un thread da   tutti i thread in competizione per la serratura   (possibilmente scegliendo lo stesso thread   che ha appena rilasciato il GIL - non è così   avere alcun controllo su quale thread   viene scelto); quel thread acquisisce il   GIL e poi corre per altri dieci   bytecode.

e

  

Nota attentamente che solo il GIL   limita il puro codice Python. estensioni   (librerie Python esterne di solito   scritto in C) può essere scritto quello   rilasciare il blocco, che quindi consente   l'interprete Python da eseguire   separatamente dall'estensione fino al   l'estensione riattiva il blocco.

Sembra che GIL fornisca solo un minor numero di istanze possibili per un cambio di contesto e fa sì che i sistemi multi-core / processore si comportino come un singolo core, rispetto a ciascuna istanza dell'interprete python, quindi sì, è comunque necessario utilizzare meccanismi di sincronizzazione .

Pensaci in questo modo:

Su un singolo computer processore, il multithreading si verifica sospendendo un thread e avviandone un altro abbastanza velocemente da far sembrare che sia in esecuzione allo stesso tempo. Questo è come Python con GIL: solo un thread è mai effettivamente in esecuzione.

Il problema è che il thread può essere sospeso ovunque, ad esempio, se voglio calcolare b = (a + b) * 3, questo potrebbe produrre istruzioni come questa:

1    a += b
2    a *= 3
3    b = a

Ora, supponiamo che sia in esecuzione in un thread e che il thread sia sospeso dopo una riga 1 o 2 e quindi un altro thread si avvia ed esegue:

b = 5

Quindi quando riprende l'altro thread, b viene sovrascritto dai vecchi valori calcolati, che probabilmente non è quello che ci si aspettava.

Quindi puoi vedere che anche se NON SONO REALMENTE in esecuzione allo stesso tempo, devi comunque bloccare.

È comunque necessario utilizzare i blocchi (il codice potrebbe essere interrotto in qualsiasi momento per eseguire un altro thread e ciò potrebbe causare incoerenze nei dati). Il problema con GIL è che impedisce al codice Python di utilizzare più core contemporaneamente (o più processori se disponibili).

I blocchi sono ancora necessari. Proverò a spiegare perché sono necessari.

Qualsiasi operazione / istruzione viene eseguita nell'interprete. GIL assicura che l'interprete sia trattenuto da un singolo thread in un particolare istante di tempo . E il tuo programma con più thread funziona in un singolo interprete. In ogni particolare istante di tempo, questo interprete è tenuto da un singolo thread. Significa che solo il thread che contiene l'interprete è in esecuzione in qualsiasi momento.

Supponiamo che ci siano due thread, diciamo t1 e t2, ed entrambi vogliono eseguire due istruzioni che leggono il valore di una variabile globale e lo incrementano.

#increment value
global var
read_var = var
var = read_var + 1

Come indicato sopra, GIL garantisce solo che due thread non possano eseguire contemporaneamente un'istruzione, il che significa che entrambi i thread non possono eseguire read_var = var in un particolare istante di tempo. Ma possono eseguire le istruzioni una dopo l'altra e puoi ancora avere problemi. Considera questa situazione:

  • Supponiamo che read_var sia 0.
  • GIL è trattenuto dal thread t1.
  • t1 esegue read_var = var . Pertanto, read_var in t1 è 0. GIL garantirà che questa operazione di lettura non verrà eseguita per nessun altro thread in questo momento.
  • GIL è dato al thread t2.
  • t2 esegue read_var = var . Ma read_var è ancora 0. Quindi, read_var in t2 è 0.
  • GIL è dato a t1.
  • t1 esegue var = read_var + 1 e var diventa 1.
  • GIL è dato a t2.
  • t2 pensa read_var = 0, perché è quello che legge.
  • t2 esegue var = read_var + 1 e var diventa 1.
  • La nostra aspettativa era che var diventasse 2.
  • Quindi, un blocco deve essere usato per mantenere sia la lettura che l'incremento come un'operazione atomica.
  • La risposta di Will Harris lo spiega attraverso un esempio di codice.

Un po 'di aggiornamento dall'esempio di Will Harris:

class Withdraw(threading.Thread):  
def run(self):            
    for _ in xrange(1000000):  
        global shared_balance  
        if shared_balance >= 100:
          balance = shared_balance
          balance -= 100  
          shared_balance = balance

Metti una dichiarazione di verifica del valore nel ritiro e non vedo più il negativo e gli aggiornamenti sembrano coerenti. La mia domanda è:

Se GIL impedisce che un solo thread possa essere eseguito in qualsiasi momento atomico, allora quale sarebbe il valore stantio? Se nessun valore non aggiornato, perché è necessario il blocco? (Supponendo che parliamo solo di puro codice Python)

Se capisco correttamente, il controllo delle condizioni di cui sopra non funzionerebbe in un ambiente di thread reale . Quando più thread vengono eseguiti contemporaneamente, è possibile creare un valore non aggiornato, quindi l'incongruenza dello stato di condivisione, quindi è necessario un blocco. Ma se Python consente davvero solo un thread alla volta (suddivisione del tempo), allora non dovrebbe esistere un valore stantio, giusto?

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top