Является ли оператор + = поточно-ориентированным в Python? - PullRequest
41 голосов
/ 11 ноября 2009

Я хочу создать не-потокобезопасный кусок кода для экспериментов, и это те функции, которые будут вызывать 2 потока.

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

Безопасен ли этот код?

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

Если это потокобезопасный, как я могу сделать его явно не потокобезопасным?

Ответы [ 8 ]

86 голосов
/ 12 ноября 2009

Нет, этот код абсолютно, очевидно, не потокобезопасен.

import threading

i = 0

def test():
    global i
    for x in range(100000):
        i += 1

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 1000000, i

последовательно терпит неудачу.

i + = 1 преобразуется в четыре кода операции: загрузить i, загрузить 1, добавить два и сохранить его обратно в i. Интерпретатор Python переключает активные потоки (выпуская GIL из одного потока, чтобы другой поток мог его иметь) через каждые 100 кодов операций. (Оба они являются деталями реализации.) Условие состязания возникает, когда между загрузкой и сохранением происходит прерывание по 100 опкодам, что позволяет другому потоку начать увеличивать счетчик. Когда он возвращается к приостановленному потоку, он продолжает со старым значением «i» и отменяет приращения, выполняемые другими потоками за это время.

Сделать его потокобезопасным просто; добавить блокировку:

#!/usr/bin/python
import threading
i = 0
i_lock = threading.Lock()

def test():
    global i
    i_lock.acquire()
    try:
        for x in range(100000):
            i += 1
    finally:
        i_lock.release()

threads = [threading.Thread(target=test) for t in range(10)]
for t in threads:
    t.start()

for t in threads:
    t.join()

assert i == 1000000, i
28 голосов
/ 11 ноября 2009

(примечание: вам потребуется global c в каждой функции, чтобы ваш код работал.)

Безопасен ли этот кодовый поток?

Нет. Только одна инструкция байт-кода является «атомарной» в CPython, и += не может привести к одному операционному коду, даже если используемые значения являются простыми целыми числами:

>>> c= 0
>>> def inc():
...     global c
...     c+= 1

>>> import dis
>>> dis.dis(inc)

  3           0 LOAD_GLOBAL              0 (c)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD         
              7 STORE_GLOBAL             0 (c)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

Таким образом, один поток может добраться до индекса 6 с загруженными c и 1, отказаться от GIL и пустить другой поток, который выполняет inc и спит, возвращая GIL первому потоку, который теперь имеет неправильное значение .

В любом случае, атомарным является деталь реализации, на которую не следует полагаться. Байт-коды могут измениться в будущих версиях CPython, и результаты будут совершенно другими в других реализациях Python, которые не зависят от GIL. Если вам нужна безопасность нитей, вам нужен механизм блокировки.

15 голосов
/ 20 июля 2011

Чтобы быть уверенным, я рекомендую использовать замок:

import threading

class ThreadSafeCounter():
    def __init__(self):
        self.lock = threading.Lock()
        self.counter=0

    def increment(self):
        with self.lock:
            self.counter+=1


    def decrement(self):
        with self.lock:
            self.counter-=1

Синхронизированный декоратор также может помочь сохранить код легким для чтения.

10 голосов
/ 11 ноября 2009

Легко доказать, что ваш код не безопасен для потоков . Вы можете увеличить вероятность увидеть состояние гонки, используя режим сна в критических частях (это просто моделирует медленный процессор). Однако, если вы выполняете код достаточно долго, вы должны в конечном итоге увидеть состояние гонки в любом случае.

from time import sleep
c = 0

def increment():
  global c
  c_ = c
  sleep(0.1)
  c = c_ + 1

def decrement():
  global c
  c_ = c
  sleep(0.1)
  c  = c_ - 1
4 голосов
/ 11 ноября 2009

Одиночные коды операций являются поточно-ориентированными из-за GIL, но не более:

import time
class something(object):
    def __init__(self,c):
        self.c=c
    def inc(self):
        new = self.c+1 
        # if the thread is interrupted by another inc() call its result is wrong
        time.sleep(0.001) # sleep makes the os continue another thread
        self.c = new


x = something(0)
import threading

for _ in range(10000):
    threading.Thread(target=x.inc).start()

print x.c # ~900 here, instead of 10000

Каждый ресурс, общий для нескольких потоков , должен иметь блокировку.

4 голосов
/ 11 ноября 2009

Краткий ответ: нет.

Длинный ответ: обычно нет.

В то время как GIL CPython делает одиночные коды операций потоко-безопасными , это не общее поведение. Вы не можете предполагать, что даже такие простые операции, как сложение, являются атомарной инструкцией. Добавление может выполняться только наполовину, когда запускается другой поток.

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

2 голосов
/ 11 ноября 2009

Если вы действительно хотите сделать свой код не потоко-безопасным, и у вас есть хорошие шансы на то, что «плохие» вещи действительно будут происходить без ваших попыток, как десять тысяч раз (или один раз, когда вы реально ) не хотите, чтобы происходили "плохие" вещи, вы можете "дрожать" в своем коде с явным сном:

def íncrement():
    global c
    x = c
    from time import sleep
    sleep(0.1)
    c = x + 1
0 голосов
/ 11 ноября 2009

Вы уверены, что функции увеличения и уменьшения выполняются без ошибок?

Я думаю, это должно вызвать UnboundLocalError, потому что вы должны явно указать Python, что вы хотите использовать глобальную переменную с именем 'c'.

Так что измените приращение (также уменьшите) на следующее:

def increment():
    global c
    c += 1

Я думаю, твой код небезопасен. Эта статья о механизмах синхронизации потоков в Python может быть полезна.

...