Подтвердить что ты не робот

Является ли оператор + = потокобезопасным в Python?

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

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

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

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

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

4b9b3361

Ответ 1

Одиночные коды операций являются потокобезопасными из-за 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

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

Ответ 2

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

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

Ответ 3

(обратите внимание: вам понадобится 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 в первый поток, который теперь имеет неправильное значение.

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

Ответ 4

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

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

Ответ 7

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

def íncrement():
    global c
    x = c
    from time import sleep
    sleep(0.1)
    c = x + 1

Ответ 8

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

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

Итак, измените инкремент (также уменьшите) на следующее:

def increment():
    global c
    c += 1

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