2009-08-21 12 views
22

Czy dostęp/zmiana wartości słownika jest bezpieczna dla wątków?Używanie globalnego słownika z wątkami w Pythonie

Mam globalny słownik foo i wiele wątków z identyfikatorami id1, id2, ..., idn. Czy można uzyskać dostęp i zmienić wartości foo bez przydzielania blokady, jeśli wiadomo, że każdy wątek będzie działał tylko z jego wartością związaną z identyfikatorem, np. Wątek z id1 będzie działał tylko z foo[id1]?

+0

Jesteś ** używają CPython, prawda? – voyager

+0

@voyager: tak, używam CPython. – Alex

Odpowiedz

34

Zakładając, że CPython: Tak i nie. W rzeczywistości można bezpiecznie pobierać/przechowywać wartości ze wspólnego słownika w tym sensie, że wiele współbieżnych żądań odczytu/zapisu nie spowoduje uszkodzenia słownika. Wynika to z globalnej blokady tłumacza ("GIL") utrzymywanej przez implementację. Czyli:

nawlec trwania:

a = global_dict["foo"] 

Wątek B trwania:

global_dict["bar"] = "hello" 

Wątek C trwania:

global_dict["baz"] = "world" 

nie będzie uszkodzony słownika, nawet jeśli wszystko trzy próby dostępu mają miejsce w tym samym czasie. Tłumacz dokona ich serializacji w jakiś nieokreślony sposób.

Jednak wyniki z następującej sekwencji jest zdefiniowana:

nawlec:

if "foo" not in global_dict: 
    global_dict["foo"] = 1 

gwintu B:

global_dict["foo"] = 2 

jako test/zestaw nici A nie jest atomowy (warunek wyścigu "czas czekania/czas użycia").Tak, to jest generalnie najlepiej, jeśli zablokować rzeczy:

lock = RLock() 

def thread_A(): 
    lock.acquire() 
    try: 
     if "foo" not in global_dict: 
      global_dict["foo"] = 1 
    finally: 
     lock.release() 

def thread_B(): 
    lock.acquire() 
    try: 
     global_dict["foo"] = 2 
    finally: 
     lock.release() 
+0

Czy 'global_dict.setdefault (" foo ", 1)' w 'wątku A' powoduje, że blokada jest niepotrzebna? – Claudiu

+0

Czy rozumiem to poprawnie. Dopóki dodam do słownika bez modyfikacji, jest to bezpieczne. czyli dyktować ['a'] = 1 w wątku a i dyktować ['b'] = 2 w wątku b jest w porządku, ponieważ klucze a i b nie są takie same? – Cripto

+0

@ user1048138 - Nie. To, co jest bezpieczne, a co nie, zależy od zastosowania. Pomyśl o klasie, która ma pola 'a' i' b' oraz niezmiennik, że dokładnie jedno z tych pól nie ma wartości "Brak", a drugie "Brak". O ile dostęp nie jest prawidłowo zblokowany, dowolna przypadkowa kombinacja 'a jest [nie] None' i' b is [not] Żaden' nie może być obserwowany z wyraźnym naruszeniem niezmiennika, jeśli użyto tylko "naiwnego" gettera/settera (pomyśl : def def set_a (self, a): self.a = a; self.b = None jeśli a nie jest None Brak self.b' - współbieżny wątek może obserwować nielegalne stany podczas wykonywania) – Dirk

3

Zajmuje się tym GIL, jeśli używasz CPython.

globalny blokady interpretera

Blokada wykorzystywane przez Pythona nici, aby zapewnić, że tylko jeden z wątków wykonywany na maszynie wirtualnej CPython jednocześnie. Upraszcza to implementację CPython, zapewniając, że żadne dwa procesy nie mają dostępu do tej samej pamięci w tym samym czasie. Zablokowanie całego interpretera ułatwia interpreterowi wielowątkowe, kosztem dużej równoległości zapewnianej przez maszyny wieloprocesorowe. W przeszłości podjęto wysiłki, aby stworzyć interpreter "wolny od gwintów" (taki, który blokuje współdzielone dane z dużo większą ziarnistością), ale jak dotąd żaden z nich nie zakończył się sukcesem, ponieważ wydajność została osiągnięta we wspólnym przypadku pojedynczego procesora.

Zobacz are-locks-unnecessary-in-multi-threaded-python-code-because-of-the-gil.

+0

To dotyczy jednak tylko CPython. –

+0

O ile nie używa on Jython lub IronPython. – voyager

+0

@Bastien Léonard: Uderz mnie w to :) – voyager

20

najlepsze, najbezpieczniejsze, przenośny sposób, aby każda praca wątek z niezależnym danych jest:

import threading 
tloc = threading.local() 

Teraz każdy wątek pracuje z całkowicie niezależny obiekt tloc, mimo że jest to nazwa globalna. Wątek może uzyskać i ustawić atrybuty na tloc, należy użyć tloc.__dict__, jeśli specjalnie potrzebuje słownika itp.

Lokalne przechowywanie wątków dla wątku znika na końcu wątku; aby wątki zapisywały swoje końcowe wyniki, mają ich wyniki, zanim zakończą, do wspólnej instancji Queue.Queue (która jest wewnętrznie bezpieczna dla wątków). Podobnie, początkowe wartości danych, dla których wątek ma pracować, mogą być argumentami przekazanymi, gdy wątek jest uruchamiany, lub mogą być pobrane z Queue.

Inne półpieczone podejścia, takie jak nadzieja, że ​​operacje wyglądające na atomowe rzeczywiście są atomowe, mogą zadziałać w konkretnych przypadkach w danej wersji i wydaniu Pythona, ale mogą łatwo zostać zerwane przez uaktualnienia lub porty. Nie ma prawdziwego powodu, aby ryzykować takie problemy, gdy właściwa, czysta i bezpieczna architektura jest tak łatwa do zorganizowania, przenośna, poręczna i szybka.

11

Ponieważ potrzebowałem czegoś podobnego, wylądowałem tutaj. I podsumować swoje odpowiedzi w tym krótkim fragmencie:

#!/usr/bin/env python3 

import threading 

class ThreadSafeDict(dict) : 
    def __init__(self, * p_arg, ** n_arg) : 
     dict.__init__(self, * p_arg, ** n_arg) 
     self._lock = threading.Lock() 

    def __enter__(self) : 
     self._lock.acquire() 
     return self 

    def __exit__(self, type, value, traceback) : 
     self._lock.release() 

if __name__ == '__main__' : 

    u = ThreadSafeDict() 
    with u as m : 
     m[1] = 'foo' 
    print(u) 

jako takie, można użyć with konstrukt posiadać blokadę podczas błahy w swojej dict()

1

Jak to działa ?:

>>> import dis 
>>> demo = {} 
>>> def set_dict(): 
...  demo['name'] = 'Jatin Kumar' 
... 
>>> dis.dis(set_dict) 
    2   0 LOAD_CONST    1 ('Jatin Kumar') 
       3 LOAD_GLOBAL    0 (demo) 
       6 LOAD_CONST    2 ('name') 
       9 STORE_SUBSCR 
      10 LOAD_CONST    0 (None) 
      13 RETURN_VALUE 

Każda z powyższych instrukcji jest wykonywana z blokadą blokady GIL, a instrukcja STORE_SUBSCR dodaje/aktualizuje parę klucz + wartość w słowniku. Więc widzisz, że aktualizacja słownika jest atomowa, a zatem wątek bezpieczny.

Powiązane problemy