2016-07-08 59 views
21

Se ho un intero i, non è sicuro di fare i += 1 su più thread:L'estensione di un elenco Python (ad es. L + = [1]) è garantita per essere thread-safe?

>>> i = 0 
>>> def increment_i(): 
...  global i 
...  for j in range(1000): i += 1 
... 
>>> threads = [threading.Thread(target=increment_i) for j in range(10)] 
>>> for thread in threads: thread.start() 
... 
>>> for thread in threads: thread.join() 
... 
>>> i 
4858 # Not 10000 

Tuttavia, se ho una lista l, sembra sicuro di fare l += [1] su più thread:

>>> l = [] 
>>> def extend_l(): 
...  global l 
...  for j in range(1000): l += [1] 
... 
>>> threads = [threading.Thread(target=extend_l) for j in range(10)] 
>>> for thread in threads: thread.start() 
... 
>>> for thread in threads: thread.join() 
... 
>>> len(l) 
10000 

È l += [1] garantito per essere thread-safe? Se è così, questo vale per tutte le implementazioni Python o solo per CPython?

Edit: Sembra che l += [1] è thread-safe, ma non è l = l + [1] ...

>>> l = [] 
>>> def extend_l(): 
...  global l 
...  for j in range(1000): l = l + [1] 
... 
>>> threads = [threading.Thread(target=extend_l) for j in range(10)] 
>>> for thread in threads: thread.start() 
... 
>>> for thread in threads: thread.join() 
... 
>>> len(l) 
3305 # Not 10000 
+1

È davvero sorprendente per me, non mi aspetto che succeda così. Spero che qualcuno fornisca una chiara spiegazione di ciò. –

+2

Anche se ho messo a monte questo, penso che l'affermazione "Quali operazioni in Python sono garantite per essere thread-safe e quali no?" condanna la domanda di chiusura generale. Potresti riformularlo? – Bathsheba

+1

In attesa di alcuni vincoli aggiunti alla domanda, ho trovato di nuovo l'effBot: [Quali tipi di mutazione del valore globale sono thread-safe?] (Http://effbot.org/pyfaq/what-kinds-of-global-value-mutation -are-thread-safe.htm) una lettura interessante.Suggerisco di riformulare in: "Quali tipi di mutazione del valore globale sono thread-safe" per essere un bel rischio ;-) Per quanto riguarda l'esempio della lista: L'elenco è thread-safe nelle sue operazioni, ma i dati stessi non sono "sicuri" dal contenitore. Quindi qualsiasi accesso alla lista che cambia il contenuto dell'elemento subirà l'intero '+ = 1'. – Dilettant

risposta

14

Non c'è una risposta ;-) felice di questo. Non c'è nulla di garantito in tutto ciò, che è possibile confermare semplicemente osservando che il manuale di riferimento di Python non garantisce l'atomicità.

In CPython è una questione di pragmatica. Come dice una parte tagliata dell'articolo di effbot,

In teoria, ciò significa che una contabilità esatta richiede una comprensione esatta dell'implementazione bytecode di PVM [Python Virtual Machine].

E questa è la verità. Un esperto sa CPython L += [x] è atomica perché sanno tutti i seguenti:

  • += compila a un INPLACE_ADD bytecode.
  • L'implementazione di INPLACE_ADD per gli oggetti elenco è interamente scritta in C (nessun codice Python si trova nel percorso di esecuzione, pertanto GIL non può essere rilasciato tra i byte).
  • In listobject.c, l'implementazione di INPLACE_ADD è la funzione list_inplace_concat(), e nulla durante la sua esecuzione deve eseguire qualsiasi codice Python utente (se così fosse, la GIL potrebbe essere nuovamente rilasciata).

Questo può sembrare incredibilmente difficile da mantenere dritto, ma per qualcuno con la conoscenza di effbot degli interni di CPython (al momento in cui ha scritto quell'articolo), in realtà non lo è. In realtà, dal momento che la profondità della conoscenza, è tutto abbastanza ovvio ;-)

Così come una questione di pragmatica, gli esperti CPython hanno sempre fatto affidamento su liberamente che "le operazioni che 'guardare atomica' in realtà dovrebbe essere atomica" e che ha anche guidato alcune decisioni linguistiche. Ad esempio, un'operazione manca dalla lista di effbot (aggiunto alla lingua dopo aver scritto quell'articolo):

x = D.pop(y) # or ... 
x = D.pop(y, default) 

Un argomento (al momento) a favore di aggiungere dict.pop() era proprio che l'attuazione ovvia C sarebbe atomica , mentre l'a-uso (al momento) alternativa:

x = D[y] 
del D[y] 

non era atomica (il recupero e la cancellazione sono effettuate tramite bytecodes distinti, in modo thread possono passare tra di loro).

Ma i documenti non hanno mai detto.pop() era atomico, e mai lo farà. Questo è un genere di "adulti consenzienti": se sei esperto abbastanza da sfruttarlo consapevolmente, non hai bisogno di tenere le mani. Se non sei esperto abbastanza, si applica l'ultima frase dell'articolo di effbot:

In caso di dubbio, utilizzare un mutex!

Di necessità pragmatica, sviluppatori principali sarà mai rompere l'atomicità esempi di effbot (o di D.pop() o D.setdefault()) in CPython. Altre implementazioni non sono affatto obbligate a imitare queste scelte pragmatiche, però. Infatti, poiché l'atomicità in questi casi si basa sulla forma specifica di bytecode di CPython combinata con l'uso di CPython di un blocco dell'interprete globale che può essere rilasciato solo tra bytecode, è essere un vero dolore per altre implementazioni per imitarli.

E non si sa mai: alcune versioni future di CPython possono rimuovere anche GIL! Ne dubito, ma è teoricamente possibile. Ma se ciò accade, scommetto che verrà mantenuta anche una versione parallela che mantiene il GIL, perché un sacco di codice (specialmente i moduli di estensione scritti in C) si appoggia anche a GIL per la sicurezza dei thread.

Vale la pena ripetere:

In caso di dubbio, utilizzare un mutex!

+0

Grazie per questa risposta completa. Il riassunto sembra essere "o usare un mutex, o fare affidamento su dettagli di implementazione non documentati di CPython". Presumibilmente c'è un sacco di codice Python che fa in modo che quest'ultimo, basato sulle "operazioni che sembrano atomiche", dovrebbe essere davvero una "linea guida atomica". Come dici tu, questo significa che CPython non può mai rompere l'atomicità di certe operazioni. Altre implementazioni potrebbero (e potrebbero trovare più facile) farlo a scapito di rompere questo codice. – user200783

+0

Tuttavia, sembra che le implementazioni Python alternative stiano cercando di eguagliare le linee guida di atomicità di CPython: ho riprodotto i risultati nella domanda su PyPy, IronPython e Jython. Ho un grande rispetto per gli sviluppatori di questi runtime: essere altamente compatibili con CPython per poter eseguire il maggior numero possibile di codice Python, anche il codice che si basa su dettagli di implementazione non documentati, deve essere un'enorme quantità di lavoro. – user200783

+0

Ahimè, non c'è modo di _know_ senza diventare un esperto in ogni implementazione: i test da soli non possono mai dimostrare l'assenza di cattivi comportamenti legati alla razza. Ad esempio, nell'implementazione di CPython ci sono stati sottili bug di threading che non sono stati scoperti per anni, fino a quando una qualche improbabile combinazione di HW, SO e carico di lavoro si è appena verificata per provocare razze che erano sempre possibili ma mai viste prima. Non comune, sì, ma "_know_" è una parola forte ;-) –

9

Da http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm:

Operazioni che sostituiscono altri oggetti possono richiamare quegli altri oggetti Metodo __del__ quando il loro conteggio dei riferimenti raggiunge lo zero e ciò può influenzare le cose. Questo è particolarmente vero per gli aggiornamenti di massa di dizionari e liste.

Le seguenti operazioni sono tutti atomica (L, L1, L2 sono elenchi, D, D1, D2 sono dicts, x, y sono oggetti, i, j sono interi):

L.append(x) 
L1.extend(L2) 
x = L[i] 
x = L.pop() 
L1[i:j] = L2 
L.sort() 
x = y 
x.field = y 
D[x] = y 
D1.update(D2) 
D.keys() 

Questi aren' t:

i = i+1 
L.append(L[-1]) 
L[i] = L[j] 
D[x] = D[x] + 1 

Sopra è puramente CPython specifico e può variare tra i diversi implemenation pitone come PyPy.

Tra l'altro c'è un problema aperto per documentare le operazioni di Python atomiche - https://bugs.python.org/issue15339

+1

L'OP ha anche chiesto altre implementazioni Python e sarei interessato a sapere anche questo ... Il GIL funziona in modo identico per tutte queste operazioni su tutte le implementazioni (che in realtà hanno un GIL)? –

+0

Grazie per questi link. Il numero 15339 ha 4 anni - non sembra che qualcuno abbia fretta di documentare qualcosa su thread-safety in Python. Questa fondamentale proprietà del linguaggio rimane un dettaglio di implementazione della sua implementazione di riferimento. – user200783