Використання глобальної змінної з потоком


84

Як поділитися глобальною змінною з потоком?

Мій приклад коду 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()

Я не знаю, як змусити два потоки ділитися однією змінною.

Відповіді:


97

Вам просто потрібно оголосити aяк глобальний в 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()

Це вирішує велику проблему. І, здається, в значній мірі правильний підхід до цього.
Абхідемон,

Це спосіб, який я використовую для вирішення проблеми синхронізації.
Zhang LongQI

1
У мене є кілька запитань. По-перше, якщо у мене є кілька змінних, якими я ділюсь між потоками, чи потрібна мені окрема черга для кожної змінної? По-друге, чому черги у вищезазначеній програмі синхронізуються? Чи не повинен кожен діяти як локальна копія у кожній функції?

Це старе, але я все одно відповідаю. Сама черга не синхронізована, не більше ніж змінна a. Це типова поведінка блокування черги, яка створює синхронізацію. Оператор a = q.get()заблокує (зачекає), поки не стане доступним значення a. Змінна qлокальна: якщо їй призначити інше значення, це відбуватиметься лише локально. Але черга, призначена їй у коді, - це та, яка визначена в основному потоці.

1
Не завжди потрібно використовувати чергу для обміну інформацією між потоками. Приклад у відповіді Чепнера цілком чудовий. Крім того, черга не завжди є правильним інструментом. Черга корисна, наприклад, якщо ви хочете заблокувати, поки значення не стане доступним. Марно, якщо дві нитки змагаються на спільному ресурсі. Нарешті, глобальні змінні не найгірші у потоках. Вони насправді можуть бути більш природними. Наприклад, ваш потік може бути просто блоком коду, скажімо, циклом, який потребує власного процесу. Таким чином, локальна область створюється штучно, коли ви вводите цикл у функцію.

5

Слід розглянути можливість використання замку, наприклад threading.Lock. Додаткову інформацію див. У розділі блоки об’єктів .

Прийнята відповідь МОЖЕ надрукувати 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 у thread1, дозволяючи досягти недосяжного коду. Я організував ідеї попередніх публікацій у повну демонстраційну програму (нижче), яку я запускав з 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.

З парою придбання / вивільнення блокування в thread1 я взагалі не сподівався побачити повідомлення, але воно є. Після того, як я вставив пару придбання / звільнення блокування також у thread2, повідомлення більше не з'являлося. У задньому підписі, оператор збільшення в thread2, ймовірно, також неатомний.


1
Вам потрібні замки в обох потоках, оскільки це спільні «консультативні замки» (не «обов’язкові»). Ви маєте рацію в тому, що оператор приросту неатомний.
Дарконаут

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. Але він зупиняється і переходить до наступного потоку Тут, при T + 1 виконується a+1зі старим значенням a a == 4,. Отже, він обчислює 5. Перейти назад (при T + 2), потік 1 і записати a=104в пам’ять. Повернемося до потоку 2, час T + 3 і запишемо a=5в пам'ять. Вуаля! Наступна інструкція друку буде друкувати 5 замість 104.

ДУЖЕ неприємна помилка, яку можна відтворити та зловити.


Будь ласка, подумайте також про додавання правильної реалізації. Це було б дуже корисно для тих, хто вчиться обмінюватися даними між потоками.
Й.С.

1
Додано до списку "todo" :)
visoft
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.