Co to jest „lokalna pamięć wątkowa” w Pythonie i dlaczego jej potrzebuję?

100

W szczególności w Pythonie, w jaki sposób zmienne są udostępniane między wątkami?

Chociaż korzystałem threading.Threadwcześniej, nigdy tak naprawdę nie rozumiałem ani nie widziałem przykładów udostępniania zmiennych. Czy są one wspólne dla głównego wątku i dzieci, czy tylko wśród dzieci? Kiedy muszę używać lokalnego magazynu wątków, aby uniknąć tego udostępniania?

Widziałem wiele ostrzeżeń dotyczących synchronizowania dostępu do współdzielonych danych między wątkami przy użyciu blokad, ale nie widziałem jeszcze naprawdę dobrego przykładu problemu.

Z góry dziękuję!

Mikrofon
źródło
2
Tytuł nie pasuje do pytania. Pytanie dotyczy udostępniania zmiennych między wątkami, tytuł sugeruje, że dotyczy on w szczególności lokalnego przechowywania
wątków
2
@Casebash: z brzmienia tego pytania Mike przeczytał, że TLS jest niezbędny, aby uniknąć problemów spowodowanych przez współdzielone dane, ale nie było jasne, które dane są udostępniane domyślnie, komu zostały udostępnione i jak zostały udostępnione. Dostosowałem tytuł, aby lepiej pasował do pytania.
Shog9,

Odpowiedzi:

83

W Pythonie wszystko jest współdzielone, z wyjątkiem zmiennych lokalnych funkcji (ponieważ każde wywołanie funkcji otrzymuje własny zestaw zmiennych lokalnych, a wątki są zawsze oddzielnymi wywołaniami funkcji). A nawet wtedy tylko same zmienne (nazwy, które odnoszą się do obiektów) są lokalne dla funkcji; same obiekty są zawsze globalne i wszystko może się do nich odnosić. ThreadObiektu dla konkretnego wątku nie jest szczególny przedmiot w tym względzie. Jeśli przechowujesz Threadobiekt w miejscu, do którego mają dostęp wszystkie wątki (jak zmienna globalna), wszystkie wątki będą miały dostęp do tego jednego Threadobiektu. Jeśli chcesz atomowo zmodyfikować wszystko , do czego inny wątek ma dostęp, musisz zabezpieczyć go blokadą. Wszystkie wątki muszą oczywiście mieć tę samą blokadę, inaczej nie byłoby to zbyt skuteczne.

Jeśli chcesz mieć rzeczywistą pamięć lokalną dla wątków, właśnie tam threading.localjest. Atrybuty threading.localnie są współdzielone między wątkami; każdy wątek widzi tylko atrybuty, które sam tam umieścił. Jeśli jesteś ciekawy jego implementacji, źródło znajduje się w _threading_local.py w bibliotece standardowej.

Thomas Wouters
źródło
1
Czy możesz podać więcej szczegółów na temat następującego zdania? „Jeśli chcesz atomowo zmodyfikować wszystko, czego nie utworzyłeś tylko w tym samym wątku i nie przechowałeś nigdzie innego wątku, do którego mógłby się dostać, musisz zabezpieczyć to blokadą”.
changyuheng
@changyuheng: Oto wyjaśnienie, czym są działania atomowe: cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Tom Busby,
1
@TomBusby: Jeśli nie ma innych wątków, które mogą się do niego dostać, dlaczego musimy go zabezpieczyć blokadą, czyli dlaczego musimy uczynić proces atomowym?
changyuheng
2
Czy możesz podać krótki przykład: „same obiekty są zawsze globalne i wszystko może się do nich odnosić”. Odsyłając zakładasz, że masz na myśli przeczytanie, a nie przypisanie / dołączenie?
zmienna
@variable: Myślę, że on oznacza, że wartości nie mają zakresu
user1071847
75

Rozważ następujący kod:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread, local

data = local()

def bar():
    print("I'm called from", data.v)

def foo():
    bar()

class T(Thread):
    def run(self):
        sleep(random())
        data.v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); T (). Start ()
Dzwoniono z Thread-2
Dzwoniono z Thread-1 

Tutaj Threading.local () jest używany jako szybki i brudny sposób na przekazanie niektórych danych z run () do bar () bez zmiany interfejsu foo ().

Pamiętaj, że użycie zmiennych globalnych nie załatwi sprawy:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread

def bar():
    global v
    print("I'm called from", v)

def foo():
    bar()

class T(Thread):
    def run(self):
        global v
        sleep(random())
        v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); T (). Start ()
Dzwoniono z Thread-2
Dzwoniono z Thread-2 

W międzyczasie, gdybyś mógł pozwolić sobie na przekazywanie tych danych jako argumentu funkcji foo () - byłby to bardziej elegancki i dobrze zaprojektowany sposób:

from threading import Thread

def bar(v):
    print("I'm called from", v)

def foo(v):
    bar(v)

class T(Thread):
    def run(self):
        foo(self.getName())

Ale nie zawsze jest to możliwe, gdy używasz kodu innej firmy lub źle zaprojektowanego kodu.

ahatchkins
źródło
18

Możesz utworzyć lokalny magazyn wątków za pomocą threading.local().

>>> tls = threading.local()
>>> tls.x = 4 
>>> tls.x
4

Dane przechowywane w tls będą unikalne dla każdego wątku, co pomoże zapewnić, że nie nastąpi niezamierzone udostępnianie.

Aaron Maenpaa
źródło
2

Podobnie jak w każdym innym języku, każdy wątek w Pythonie ma dostęp do tych samych zmiennych. Nie ma rozróżnienia między wątkiem głównym a wątkami podrzędnymi.

Jedyną różnicą w stosunku do Pythona jest to, że globalna blokada interpretera oznacza, że ​​tylko jeden wątek może jednocześnie uruchamiać kod Pythona. Nie jest to jednak zbyt pomocne, jeśli chodzi o synchronizację dostępu, ponieważ wszystkie typowe problemy z wywłaszczaniem nadal mają zastosowanie i musisz używać prymitywów wątków, tak jak w innych językach. Oznacza to jednak, że musisz ponownie rozważyć, czy używasz wątków do wydajności.

Nick Johnson
źródło
0

Mogę się tutaj mylić. Jeśli wiesz inaczej, wyjaśnij to, ponieważ pomogłoby to wyjaśnić, dlaczego należałoby użyć wątku local ().

Wydaje się, że to stwierdzenie nie jest błędne: „Jeśli chcesz atomowo zmodyfikować wszystko, do czego inny wątek ma dostęp, musisz zabezpieczyć to blokadą”. Myślę, że to stwierdzenie jest -> skutecznie <- słuszne, ale nie do końca trafne. Pomyślałem, że termin „atomowy” oznacza, że ​​interpreter Pythona utworzył fragment kodu bajtowego, który nie zostawiał miejsca na sygnał przerwania przesyłany do procesora.

Myślałem, że operacje atomowe to fragmenty kodu bajtowego Pythona, które nie dają dostępu do przerwań. Instrukcje Pythona, takie jak „running = True”, są niepodzielne. W tym przypadku nie musisz blokować procesora przed przerwaniami (uważam). Podział kodu bajtowego Pythona jest zabezpieczony przed przerwaniem wątku.

Kod Pythona, taki jak „thread_running [5] = True”, nie jest atomowy. Są tutaj dwa fragmenty kodu bajtowego Pythona; jeden, aby usunąć odniesienie do list () dla obiektu i inny fragment kodu bajtowego, aby przypisać wartość do obiektu, w tym przypadku „miejsce” na liście. Przerwanie można zgłosić -> pomiędzy <- dwoma kodami bajtowymi -> fragmentami <-. To było złe rzeczy.

Jak wątek local () ma się do „atomic”? Dlatego to stwierdzenie wydaje mi się mylące. Jeśli nie, czy możesz wyjaśnić?

DevPlayer
źródło
1
To wygląda na odpowiedź, ale zostało zgłoszone jako problematyczne, zakładam, że z powodu zadanych pytań. Unikałbym prośby o wyjaśnienia w odpowiedzi. Po to są komentarze.
Dharman