Czy wątek + = operator jest bezpieczny w Pythonie?

Chcę stworzyć niezabezpieczony fragment kodu do eksperymentów, a to są funkcje, które będą wywoływać 2 wątki.

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

Czy ten wątek kodu jest bezpieczny?

Jeśli nie, Czy mogę zrozumieć, dlaczego nie jest to bezpieczne dla wątków i jakiego rodzaju oświadczenia zwykle prowadzą do operacji nie - bezpiecznych dla wątków.

Jeśli jest bezpieczny dla wątku, jak mogę uczynić go jawnie nie-bezpiecznym dla wątku?

Author: danijar, 2009-11-11

8 answers

Pojedyncze opcody są bezpieczne dla wątków z powodu GIL, ale nic więcej:

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

Każdy zasób współdzielony przez wiele wątków musi mieć blokadę.

 4
Author: Jochen Ritzel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 21:26:26

Nie, ten kod jest absolutnie, ewidentnie nie threadsafe.

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

Konsekwentnie zawodzi.

I + = 1 rozwiązuje cztery opcody: load i, load 1, add the two, and store it back to i. interpreter Pythona przełącza aktywne wątki (zwalniając GIL z jednego wątku, aby inny wątek mógł go mieć) co 100 opcodów. (Oba te są szczegółami implementacji.) Warunek race występuje, gdy 100-opcode preemption dzieje się między załadowaniem i przechowywania, co pozwala na inny wątek, aby rozpocząć zwiększanie licznika. Gdy powróci do wątku zawieszonego, kontynuuje ze starą wartością " i " i cofa przyrosty uruchamiane przez inne wątki w międzyczasie.

Tworzenie threadsafe jest proste; dodaj blokadę:

#!/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
 71
Author: Glenn Maynard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 23:37:10

(Uwaga: będziesz potrzebował global c w każdej funkcji, aby Twój kod działał.)

Czy ten wątek kodu jest bezpieczny?

Nie. Tylko pojedyncza Instrukcja bajtowa jest 'atomowa' w Cpythonie, a += może nie spowodować powstania pojedynczego kodu, nawet jeśli wartości są prostymi liczbami całkowitymi:
>>> 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        

Aby jedna nić mogła dostać się do indeksu 6 z C i 1 załadowanym, oddać GIL i wpuścić inną nić, która wykonuje inc i usypia, zwracając GIL do pierwszego wątku, który teraz ma złą wartość.

W każdym razie atomic jest szczegółem implementacji, na którym nie powinieneś polegać. Bajtowe kody mogą się zmieniać w przyszłych wersjach Cpythona, a wyniki będą zupełnie inne w innych implementacjach Pythona, które nie opierają się na GIL. Jeśli potrzebujesz zabezpieczenia gwintu, potrzebujesz mechanizmu blokującego.

 28
Author: bobince,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 19:33:33

Dla pewności polecam użycie zamka:

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

Zsynchronizowany dekorator może również pomóc w utrzymaniu czytelnego kodu.

 14
Author: gillesv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-07-20 11:27:40

Łatwo udowodnić, że Twój kod jest nie bezpieczny dla wątków . Możesz zwiększyć prawdopodobieństwo, że zobaczysz stan wyścigu, używając snu w krytycznych częściach(to po prostu symuluje powolny procesor). Jeśli jednak używasz kodu wystarczająco długo, powinieneś ostatecznie zobaczyć stan wyścigu niezależnie od tego.

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
 7
Author: John La Rooy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-11-04 04:15:11

Krótka odpowiedź: Nie.

Długa odpowiedź: ogólnie nie.

Podczas gdy Gil Cpythona tworzy pojedyncze opcody thread-safe, nie jest to ogólne zachowanie. Nie można zakładać, że nawet proste operacje, takie jak dodawanie, są instrukcją atomową. Dodanie może być wykonane tylko w połowie, gdy działa inny wątek.

I jak tylko twoje funkcje uzyskają dostęp do zmiennej w więcej niż jednym kodzie, Twoje bezpieczeństwo wątku zniknie. Możesz wygenerować bezpieczeństwo wątku, jeśli owijasz swoje ciała funkcyjne w zamki . Należy jednak pamiętać, że blokady mogą być kosztowne obliczeniowo i mogą generować blokady.

 4
Author: ebo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-12 11:02:37

Jeśli naprawdę chcesz, aby Twój kod nie był bezpieczny dla wątków i masz duże szanse na to, że "złe" rzeczy faktycznie się wydarzą, bez tego, że spróbujesz dziesięć tysięcy razy (lub jeden raz, gdy naprawdę nie chcesz, aby "złe" rzeczy się wydarzyły), możesz "jitter" kodu za pomocą explicit sleeps:

def íncrement():
    global c
    x = c
    from time import sleep
    sleep(0.1)
    c = x + 1
 2
Author: Rasmus Kaj,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 19:37:02

Czy jesteś pewien, że funkcje increment i decrement wykonują się bez błędu?

Myślę, że powinno to wywołać UnboundLocalError, ponieważ musisz wyraźnie powiedzieć Pythonowi, że chcesz użyć zmiennej globalnej o nazwie 'c'.

Więc zmień przyrost ( także przyrost) na następujący:

def increment():
    global c
    c += 1

Myślę, że Twój kod jest niebezpieczny. Ten artykuł o mechanizmach synchronizacji wątków w Pythonie może być pomocny.

 0
Author: ardsrk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-11-11 19:36:53