Używanie zmiennej globalnej z wątkiem

84

Jak udostępnić zmienną globalną wątkowi?

Mój przykład kodu w Pythonie to:

from threading import Thread
import time
a = 0  #global variable

def thread1(threadname):
    #read variable "a" modify by thread 2

def thread2(threadname):
    while 1:
        a += 1
        time.sleep(1)

thread1 = Thread( target=thread1, args=("Thread-1", ) )
thread2 = Thread( target=thread2, args=("Thread-2", ) )

thread1.join()
thread2.join()

Nie wiem, jak zmusić dwa wątki do współdzielenia jednej zmiennej.

Mauro Midolo
źródło

Odpowiedzi:

97

Wystarczy zadeklarować ajako globalny in thread2, aby nie modyfikować elementu alokalnego dla tej funkcji.

def thread2(threadname):
    global a
    while True:
        a += 1
        time.sleep(1)

W programie thread1nie musisz robić nic specjalnego, o ile nie próbujesz modyfikować wartości a(co spowodowałoby utworzenie zmiennej lokalnej, która przesłania zmienną globalną; użyj, global ajeśli musisz)>

def thread1(threadname):
    #global a       # Optional if you treat a as read-only
    while a < 10:
        print a
Chepner
źródło
47

W funkcji:

a += 1

zostanie zinterpretowany przez kompilator jako assign to a => Create local variable a, co nie jest tym, czego chcesz. Prawdopodobnie zakończy się niepowodzeniem z a not initializedbłędem, ponieważ (lokalny) a rzeczywiście nie został zainicjowany:

>>> a = 1
>>> def f():
...     a += 1
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'a' referenced before assignment

Możesz dostać to, czego chcesz, ze globalsłowem kluczowym (bardzo niezadowolonym i nie bez powodu) , na przykład:

>>> def f():
...     global a
...     a += 1
... 
>>> a
1
>>> f()
>>> a
2

Generalnie jednak należy unikać używania zmiennych globalnych, które bardzo szybko wymykają się spod kontroli. Jest to szczególnie ważne w przypadku programów wielowątkowych, w których nie ma żadnego mechanizmu synchronizacji, thread1aby wiedzieć, kiedy azostał zmodyfikowany. Krótko mówiąc: wątki są skomplikowane i nie można oczekiwać intuicyjnego zrozumienia kolejności, w jakiej zachodzą zdarzenia, gdy dwa (lub więcej) wątki działają na tej samej wartości. Język, kompilator, system operacyjny, procesor ... WSZYSTKIE mogą odgrywać rolę i decydować o zmianie kolejności operacji ze względu na szybkość, praktyczność lub z jakiegokolwiek innego powodu.

Właściwym sposobem na tego typu rzeczy jest użycie narzędzi do udostępniania w Pythonie ( blokad i znajomych) lub lepiej, przesyłanie danych za pośrednictwem kolejki zamiast ich udostępniania, np. W ten sposób:

from threading import Thread
from queue import Queue
import time

def thread1(threadname, q):
    #read variable "a" modify by thread 2
    while True:
        a = q.get()
        if a is None: return # Poison pill
        print a

def thread2(threadname, q):
    a = 0
    for _ in xrange(10):
        a += 1
        q.put(a)
        time.sleep(1)
    q.put(None) # Poison pill

queue = Queue()
thread1 = Thread( target=thread1, args=("Thread-1", queue) )
thread2 = Thread( target=thread2, args=("Thread-2", queue) )

thread1.start()
thread2.start()
thread1.join()
thread2.join()
val
źródło
To rozwiązuje duży problem. I wydaje się, że jest to właściwe podejście do tego.
Abhidemon
To jest sposób, w jaki używam do rozwiązywania problemu synchronizacji.
Zhang LongQI
1
Mam kilka pytań. Po pierwsze, jeśli mam wiele zmiennych do udostępnienia między wątkami, czy potrzebuję osobnej kolejki dla każdej zmiennej? Po drugie, dlaczego kolejki w powyższym programie są synchronizowane? Czy nie powinien każdy działać jako kopia lokalna w każdej funkcji?
To jest stare, ale i tak odpowiadam. Sama kolejka nie jest zsynchronizowana, nie więcej niż zmienna a. Jest to domyślne zachowanie blokujące kolejki, które tworzy synchronizację. Instrukcja a = q.get()będzie blokować (czekać), aż wartość a będzie dostępna. Zmienna qjest lokalna: jeśli przypiszesz jej inną wartość, stanie się to tylko lokalnie. Ale kolejka przypisana do niego w kodzie to ta, która jest zdefiniowana w głównym wątku.
1
Nie zawsze jest konieczne używanie kolejki do udostępniania informacji między wątkami. Przykład w odpowiedzi Chepnera jest w porządku. Ponadto kolejka nie zawsze jest właściwym narzędziem. Kolejka jest przydatna, na przykład, jeśli chcesz zablokować, dopóki wartość nie będzie dostępna. Nie ma sensu, jeśli dwa wątki konkurują na wspólnym zasobie. Wreszcie zmienne globalne nie są najgorsze w wątkach. W rzeczywistości mogą być bardziej naturalne. Na przykład twój wątek może być po prostu blokiem kodu, powiedzmy pętlą, który potrzebuje własnego procesu. Zasięg lokalny jest więc sztucznie tworzony po umieszczeniu pętli w funkcji.
5

Należy rozważyć użycie blokady, na przykład threading.Lock. Aby uzyskać więcej informacji, zobacz obiekty zamków .

Zaakceptowana odpowiedź MOŻE wydrukować 10 wątkiem1, co nie jest tym, czego chcesz. Możesz uruchomić poniższy kod, aby łatwiej zrozumieć błąd.

def thread1(threadname):
    while True:
      if a % 2 and not a % 2:
          print "unreachable."

def thread2(threadname):
    global a
    while True:
        a += 1

Używanie zamka może uniemożliwić zmianę apodczas czytania więcej niż jeden raz:

def thread1(threadname):
    while True:
      lock_a.acquire()
      if a % 2 and not a % 2:
          print "unreachable."
      lock_a.release()

def thread2(threadname):
    global a
    while True:
        lock_a.acquire()
        a += 1
        lock_a.release()

Jeśli wątek używa zmiennej przez długi czas, dobrym wyborem jest najpierw skopiowanie jej do zmiennej lokalnej.

Jason Pan
źródło
3

Dziękuję bardzo Jason Pan za zasugerowanie tej metody. Instrukcja thread1 if nie jest atomowa, więc gdy ta instrukcja jest wykonywana, możliwe jest, aby thread2 włamał się do thread1, umożliwiając osiągnięcie nieosiągalnego kodu. Zorganizowałem pomysły z poprzednich postów w kompletny program demonstracyjny (poniżej), który uruchomiłem z Pythonem 2.7.

Jestem pewien, że dzięki dogłębnej analizie moglibyśmy uzyskać dalsze informacje, ale na razie myślę, że ważne jest, aby pokazać, co się dzieje, gdy zachowanie nieatomowe spotyka się z wątkami.

# ThreadTest01.py - Demonstrates that if non-atomic actions on
# global variables are protected, task can intrude on each other.
from threading import Thread
import time

# global variable
a = 0; NN = 100

def thread1(threadname):
    while True:
      if a % 2 and not a % 2:
          print("unreachable.")
    # end of thread1

def thread2(threadname):
    global a
    for _ in range(NN):
        a += 1
        time.sleep(0.1)
    # end of thread2

thread1 = Thread(target=thread1, args=("Thread1",))
thread2 = Thread(target=thread2, args=("Thread2",))

thread1.start()
thread2.start()

thread2.join()
# end of ThreadTest01.py

Zgodnie z przewidywaniami, podczas uruchamiania przykładu czasami dochodzi do „nieosiągalnego” kodu, co daje wynik.

Dodam, że kiedy wstawiłem parę nabycie / zwolnienie blokady do thread1, stwierdziłem, że prawdopodobieństwo wydrukowania komunikatu „nieosiągalny” zostało znacznie zmniejszone. Aby zobaczyć komunikat, zredukowałem czas uśpienia do 0,01 sekundy i zwiększyłem NN do 1000.

Z parą pozyskiwania / zwalniania blokady w wątku 1 nie spodziewałem się, że w ogóle zobaczę komunikat, ale on tam jest. Po wstawieniu pary nabycia / zwolnienia blokady również do Thread2 komunikat przestał się pojawiać. W signt tylnej instrukcja inkrementacji w wątku2 prawdopodobnie również nie jest atomowa.

Krista M Hill
źródło
1
Potrzebujesz blokad w obu wątkach, ponieważ są to kooperacyjne, „doradcze blokady” (nie „obowiązkowe”). Masz rację, ponieważ instrukcja inkrementacji nie jest atomowa.
Darkonaut
1

Cóż, działający przykład:

OSTRZEŻENIE! NIGDY NIE ROBIĆ TEGO W DOMU / PRACY! Tylko w klasie;)

Używaj semaforów, wspólnych zmiennych itp., Aby uniknąć nagłych warunków.

from threading import Thread
import time

a = 0  # global variable


def thread1(threadname):
    global a
    for k in range(100):
        print("{} {}".format(threadname, a))
        time.sleep(0.1)
        if k == 5:
            a += 100


def thread2(threadname):
    global a
    for k in range(10):
        a += 1
        time.sleep(0.2)


thread1 = Thread(target=thread1, args=("Thread-1",))
thread2 = Thread(target=thread2, args=("Thread-2",))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

i wyjście:

Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 2
Thread-1 3
Thread-1 3
Thread-1 104
Thread-1 104
Thread-1 105
Thread-1 105
Thread-1 106
Thread-1 106
Thread-1 107
Thread-1 107
Thread-1 108
Thread-1 108
Thread-1 109
Thread-1 109
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110

Gdyby czas był właściwy, a += 100operacja zostałaby pominięta:

Procesor wykonuje w punkcie T a+100i uzyskuje 104. Ale zatrzymuje się i przeskakuje do następnego wątku. Tutaj w punkcie T + 1 wykonuje się a+1ze starą wartością a a == 4,. Więc oblicza 5. Skocz wstecz (w T + 2), wątek 1 i zapisz a=104w pamięci. Wracając do wątku 2, czas wynosi T + 3 i zapisz a=5do pamięci. Voila! Następna instrukcja drukowania wypisze 5 zamiast 104.

BARDZO okropny błąd do odtworzenia i złapania.

visoft
źródło
Rozważ również dodanie poprawnej implementacji. Byłoby to bardzo pomocne dla tych, którzy uczą się udostępniać dane między wątkami.
JS.
1
Dodano do listy „
rzeczy do zrobienia