Использование глобальной переменной с потоком

86

Как передать глобальную переменную потоку?

Мой пример кода Python:

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

Я не знаю, как заставить два потока использовать одну переменную.

Мауро Мидоло
источник

Ответы:

99

Вам просто нужно объявить aкак глобальный in thread2, чтобы не изменять aлокальный для этой функции.

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

В thread1вам не нужно делать ничего особенного, пока вы не пытаетесь изменить значение a(что создало бы локальную переменную, которая затеняет глобальную; используйте, global aесли вам нужно)>

def thread1(threadname):
    #global a       # Optional if you treat a as read-only
    while a < 10:
        print a
Чепнер
источник
47

В функции:

a += 1

будет интерпретироваться компилятором как assign to a => Create local variable a, что вам не нужно. Вероятно, произойдет сбой с a not initializedошибкой, поскольку (локальный) a действительно не был инициализирован:

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

Вы можете получить то, что хотите, с globalключевым словом (очень неодобрительно и по уважительным причинам) , например:

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

В целом, однако, вам следует избегать использования глобальных переменных, которые очень быстро выходят из-под контроля. И это особенно верно для многопоточных программ, где у вас нет механизма синхронизации, чтобы вы thread1знали, когда aон был изменен. Вкратце: потоки сложны , и вы не можете ожидать интуитивного понимания порядка, в котором происходят события, когда два (или более) потока работают с одним и тем же значением. Язык, компилятор, ОС, процессор ... ВСЕ могут играть роль и принимать решение об изменении порядка операций для скорости, практичности или по любой другой причине.

Правильный способ для такого рода вещей - использовать инструменты совместного использования Python ( блокировки и друзья) или, что еще лучше, передавать данные через очередь вместо того, чтобы делиться ими, например, вот так:

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()
вал
источник
Это решает большую проблему. И кажется в значительной степени правильным подходом к этому.
Abhidemon
Именно так я и использую для решения проблемы синхронизации.
Zhang LongQI
1
У меня есть несколько вопросов. Во-первых, если у меня есть несколько переменных для совместного использования между потоками, нужна ли мне отдельная очередь для каждой переменной? Во-вторых, почему очереди в указанной выше программе синхронизируются? Разве каждая функция не должна действовать как локальная копия в каждой функции?
Это старый, но я все равно отвечаю. Сама очередь не синхронизируется, не более чем переменная a. Синхронизация создается по умолчанию при блокировании очереди. Оператор a = q.get()будет блокироваться (ждать), пока не станет доступно значение a. Переменная qявляется локальной: если вы присвоите ей другое значение, это произойдет только локально. Но очередь, назначенная ему в коде, - это та, которая определена в основном потоке.
1
Не всегда необходимо использовать очередь для обмена информацией между потоками. Пример в ответе Чепнера прекрасен. Кроме того, очередь - не всегда правильный инструмент. Очередь полезна, например, если вы хотите заблокировать, пока значение не станет доступным. Бесполезно, если два потока конкурируют за общий ресурс. Наконец, глобальные переменные не худшие в потоках. На самом деле они могут быть более естественными. Например, ваш поток может быть просто блоком кода, скажем, циклом, которому нужен собственный процесс. Таким образом, локальная область видимости создается искусственно, когда вы помещаете цикл в функцию.
5

Следует рассмотреть возможность использования блокировки, например threading.Lock. Смотрите lock-objects для получения дополнительной информации.

Принятый ответ МОЖЕТ напечатать 10 с помощью thread1, что вам не нужно. Вы можете запустить следующий код, чтобы легче понять ошибку.

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

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

Использование замка может запретить замену a при чтении более одного раза:

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

Если поток использует переменную в течение длительного времени, хорошим выбором будет сначала скопировать ее в локальную переменную.

Джейсон Пэн
источник
3

Большое спасибо Джейсону Пэну за предложение этого метода. Оператор thread1 if не является атомарным, поэтому, пока этот оператор выполняется, поток 2 может вторгнуться в поток 1, позволяя достичь недостижимого кода. Я организовал идеи из предыдущих сообщений в полную демонстрационную программу (ниже), которую я запускал с Python 2.7.

Я уверен, что с помощью некоторого вдумчивого анализа мы могли бы получить дальнейшее понимание, но пока я думаю, что важно продемонстрировать, что происходит, когда неатомарное поведение встречается с потоками.

# 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

Как и предполагалось, при выполнении примера иногда действительно достигается "недостижимый" код, что дает результат.

Просто чтобы добавить, когда я вставил пару получения / снятия блокировки в thread1, я обнаружил, что вероятность получения сообщения о «недоступности» значительно снизилась. Чтобы увидеть сообщение, я уменьшил время сна до 0,01 секунды и увеличил NN до 1000.

С парой получения / снятия блокировки в потоке 1 я вообще не ожидал увидеть сообщение, но оно есть. После того, как я вставил пару получения / снятия блокировки также в thread2, сообщение больше не появлялось. В заднем сигнале оператор приращения в thread2, вероятно, также не является атомарным.

Криста М. Хилл
источник
1
Вам нужны блокировки в обоих потоках, потому что это совместные «рекомендательные блокировки» (не «обязательные»). Вы правы в том, что оператор приращения не является атомарным.
Darkonaut
1

Ну, рабочий пример:

ПРЕДУПРЕЖДЕНИЕ! НИКОГДА НЕ ДЕЛАЙТЕ ЭТО ДОМА / НА РАБОТЕ!Только в классе;)

Используйте семафоры, общие переменные и т. Д., Чтобы избежать срочных условий.

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

и вывод:

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

Если время было правильным, a += 100 операция была бы пропущена:

Процессор выполняется в T a+100и получает 104. Но он останавливается и переходит к следующему потоку. Здесь At T + 1 выполняется a+1со старым значением a a == 4,. Таким образом, он вычисляет 5. Обратный переход (при T + 2), поток 1 и запись a=104в память. Теперь вернемся к потоку 2, время T + 3 и напишитеa=5 в память. Вуаля! Следующая инструкция печати напечатает 5 вместо 104.

ОЧЕНЬ неприятная ошибка, которую нужно воспроизвести и отловить.

Visoft
источник
Пожалуйста, подумайте также о добавлении правильной реализации. Это было бы очень полезно для тех, кто учится обмениваться данными между потоками.
JS.
1
Добавлено в список "todo" :)
visoft