Verwenden einer globalen Variablen mit einem Thread

84

Wie teile ich eine globale Variable mit dem Thread?

Mein Python-Codebeispiel lautet:

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()

Ich weiß nicht, wie ich die beiden Threads dazu bringen soll, eine Variable gemeinsam zu nutzen.

Mauro Midolo
quelle

Antworten:

97

Sie müssen nur aein globales In deklarieren thread2, damit Sie kein alokales In für diese Funktion ändern.

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

In thread1müssen Sie nichts Besonderes tun, solange Sie nicht versuchen, den Wert von zu ändern a(wodurch eine lokale Variable erstellt wird, die die globale schattiert; verwenden global aSie sie, wenn dies erforderlich ist)>

def thread1(threadname):
    #global a       # Optional if you treat a as read-only
    while a < 10:
        print a
chepner
quelle
47

In einer Funktion:

a += 1

wird vom Compiler als interpretiert assign to a => Create local variable a, was nicht das ist, was Sie wollen. Es wird wahrscheinlich mit einem a not initializedFehler fehlschlagen , da das (lokale) a tatsächlich nicht initialisiert wurde:

>>> 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

Sie könnten das bekommen, was Sie wollen, mit dem globalSchlüsselwort (sehr verpönt und aus guten Gründen) , wie folgt:

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

Im Allgemeinen sollten Sie jedoch die Verwendung globaler Variablen vermeiden , die extrem schnell außer Kontrolle geraten. Dies gilt insbesondere für Multithread-Programme, bei denen Sie keinen Synchronisationsmechanismus haben, um thread1zu wissen, wann aÄnderungen vorgenommen wurden. Kurz gesagt: Threads sind kompliziert , und Sie können kein intuitives Verständnis der Reihenfolge erwarten, in der Ereignisse auftreten, wenn zwei (oder mehr) Threads mit demselben Wert arbeiten. Die Sprache, der Compiler, das Betriebssystem, der Prozessor ... können ALLE eine Rolle spielen und entscheiden, die Reihenfolge der Operationen aus Gründen der Geschwindigkeit, der Praktikabilität oder aus anderen Gründen zu ändern.

Der richtige Weg für diese Art von Dingen besteht darin, Python-Freigabetools ( Sperren und Freunde) zu verwenden oder Daten über eine Warteschlange zu kommunizieren, anstatt sie freizugeben, z. 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
quelle
Dies löst ein großes Problem. Und scheint so ziemlich der richtige Ansatz zu sein.
Abhidemon
Auf diese Weise löse ich das Synchronisationsproblem.
Zhang LongQI
1
Ich habe einige Fragen. Erstens: Wenn ich mehrere Variablen für die gemeinsame Nutzung zwischen Threads habe, benötige ich für jede Variable eine separate Warteschlange? Zweitens, warum werden die Warteschlangen im obigen Programm synchronisiert? Sollte nicht jeder in jeder Funktion als lokale Kopie fungieren?
Das ist alt, aber ich antworte trotzdem. Die Warteschlange selbst ist nicht synchronisiert, nicht mehr als die Variable a. Es ist das Standardblockierungsverhalten der Warteschlange, das die Synchronisierung erstellt. Die Anweisung a = q.get()blockiert (wartet), bis ein Wert a verfügbar ist. Die Variable qist lokal: Wenn Sie ihr einen anderen Wert zuweisen, geschieht dies nur lokal. Die im Code zugewiesene Warteschlange ist jedoch die im Hauptthread definierte.
1
Es ist nicht immer erforderlich, eine Warteschlange zu verwenden, um Informationen zwischen Threads auszutauschen. Das Beispiel in der Antwort von chepner ist vollkommen in Ordnung. Außerdem ist eine Warteschlange nicht immer das richtige Werkzeug. Eine Warteschlange ist beispielsweise nützlich, wenn Sie blockieren möchten, bis der Wert verfügbar ist. Es ist nutzlos, wenn die beiden Threads auf einer gemeinsam genutzten Ressource miteinander konkurrieren. Schließlich sind globale Variablen in Threads nicht schlecht. Sie können in der Tat natürlicher sein. Zum Beispiel könnte Ihr Thread nur ein Codeblock sein, beispielsweise eine Schleife, die einen eigenen Prozess benötigt. Der lokale Bereich wird somit künstlich erstellt, wenn Sie die Schleife in eine Funktion einfügen.
5

Ein Schloss sollte in Betracht gezogen werden, wie z threading.Lock. Siehe Lock-Objekte für weitere Informationen.

Die akzeptierte Antwort KANN 10 von thread1 drucken, was nicht das ist, was Sie wollen. Sie können den folgenden Code ausführen, um den Fehler leichter zu verstehen.

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

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

Die Verwendung eines Schlosses kann das Ändern beim amehrmaligen Lesen verbieten :

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()

Wenn ein Thread die Variable für längere Zeit verwendet, ist es eine gute Wahl, sie zuerst in eine lokale Variable zu kopieren.

Jason Pan
quelle
3

Vielen Dank Jason Pan für den Vorschlag dieser Methode. Die if-Anweisung thread1 ist nicht atomar, sodass Thread2 während der Ausführung dieser Anweisung in Thread1 eindringen kann und nicht erreichbarer Code erreicht werden kann. Ich habe Ideen aus den vorherigen Beiträgen in einem vollständigen Demonstrationsprogramm (unten) organisiert, das ich mit Python 2.7 ausgeführt habe.

Ich bin mir sicher, dass wir mit einigen nachdenklichen Analysen weitere Erkenntnisse gewinnen können, aber im Moment denke ich, dass es wichtig ist zu demonstrieren, was passiert, wenn nicht-atomares Verhalten auf Threading trifft.

# 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

Wie vorhergesagt, wird beim Ausführen des Beispiels manchmal der "nicht erreichbare" Code tatsächlich erreicht, wodurch eine Ausgabe erzeugt wird.

Als ich ein Thread zum Erfassen / Freigeben von Sperren in Thread1 einfügte, stellte ich fest, dass die Wahrscheinlichkeit, dass die Nachricht "nicht erreichbar" gedruckt wird, stark verringert war. Um die Nachricht zu sehen, reduzierte ich die Schlafzeit auf 0,01 Sekunden und erhöhte NN auf 1000.

Mit einem Lock Acquise / Release-Paar in Thread1 habe ich nicht erwartet, dass die Nachricht überhaupt angezeigt wird, aber sie ist da. Nachdem ich ein Lock Acquise / Release-Paar auch in Thread2 eingefügt habe, wurde die Meldung nicht mehr angezeigt. Im Nachhinein ist die Inkrement-Anweisung in Thread2 wahrscheinlich auch nicht atomar.

Krista M Hill
quelle
1
Sie benötigen die Sperren in beiden Threads, da es sich um kooperative "Beratungssperren" handelt (nicht "obligatorisch"). Sie haben Recht damit, dass die Inkrement-Anweisung nicht atomar ist.
Darkonaut
1

Nun, laufendes Beispiel:

WARNUNG! TUN SIE DAS NIEMALS ZU HAUSE / ARBEITEN! Nur im Klassenzimmer;)

Verwenden Sie Semaphoren, gemeinsam genutzte Variablen usw., um Eilbedingungen zu vermeiden.

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()

und die Ausgabe:

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

Wenn das Timing richtig wäre, würde die a += 100Operation übersprungen:

Der Prozessor wird bei T ausgeführt a+100und erhält 104. Aber er stoppt und springt zum nächsten Thread. Hier wird bei T + 1 a+1mit dem alten Wert von a , a == 4. Es berechnet also 5. Springe zurück (bei T + 2), Thread 1 und schreibe a=104in den Speicher. Zurück bei Thread 2 ist die Zeit T + 3 und a=5in den Speicher schreiben . Voila! Die nächste Druckanweisung gibt 5 anstelle von 104 aus.

SEHR böser Fehler, der reproduziert und abgefangen werden muss.

visoft
quelle
Bitte beachten Sie auch das Hinzufügen einer korrekten Implementierung. Dies wäre sehr hilfreich für diejenigen, die lernen, Daten zwischen Threads auszutauschen.
JS.
1
Zur "todo" -Liste
hinzugefügt