web-dev-qa-db-ja.com

+ =演算子はPythonでスレッドセーフですか?

実験用にスレッドセーフではないコードのチャンクを作成したいのですが、これらは2つのスレッドが呼び出す関数です。

c = 0

def increment():
  c += 1

def decrement():
  c -= 1

このコードスレッドは安全ですか?

そうでない場合は、なぜそれがスレッドセーフではないのか、そしてどのようなステートメントが通常スレッドセーフでない操作につながるのかを理解できますか?.

スレッドセーフの場合、どうすれば明示的に非スレッドセーフにすることができますか?

44
nubela

単一のオペコードは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

すべて複数のスレッドで共有されるリソース必須ロックが必要です。

3
Jochen Ritzel

いいえ、このコードは絶対に、明らかにスレッドセーフではありません。

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は、4つのオペコードに解決されます。iをロードし、1をロードし、2つを追加して、iに格納します。 Pythonインタープリターは、アクティブなスレッドを(1つのスレッドから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
91
Glenn Maynard

(注:コードを機能させるには、各関数に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        

したがって、1つのスレッドがcと1がロードされた状態でインデックス6に到達し、GILを放棄し、別のスレッドを入れてincを実行してスリープし、GILを最初のスレッドに戻します。 。

いずれにせよ、アトミックとは、信頼すべきではない実装の詳細です。バイトコードはCPythonの将来のバージョンで変更される可能性があり、GILに依存しないPythonの他の実装では結果がまったく異なります。スレッドセーフが必要な場合は、ロックメカニズムが必要です。

29
bobince

確かに私はロックを使用することをお勧めします:

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

同期されたデコレータは、コードを読みやすくするのにも役立ちます。

15
gillesv

コードがスレッドセーフではないであることを証明するのは簡単です。重要な部分でスリープを使用することにより、競合状態が発生する可能性を高めることができます(これは単に遅いCPUをシミュレートするだけです)。ただし、コードを十分に長く実行すると、最終的には関係なく競合状態が表示されます。

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
10
John La Rooy

簡単な答え:いいえ。

長い答え:一般的にはそうではありません。

CPythonのGILは単一のオペコードを作成します スレッドセーフ 、これは一般的な動作ではありません。加算のような単純な操作でさえ、アトミックな命令であるとは思わないかもしれません。追加は、別のスレッドが実行されたときに半分しか実行されない場合があります。

また、関数が複数のオペコードの変数にアクセスするとすぐに、スレッドセーフは失われます。関数本体を locks でラップすると、スレッドセーフを生成できます。ただし、ロックは計算コストが高く、デッドロックが発生する可能性があることに注意してください。

4
ebo

あなたが実際にあなたのコードをnotスレッドセーフにしたいなら、そしてあなたが1万回(またはあなたが本当のときに1回)のように試みることなく実際に「悪い」ことが起こる可能性が高いならドン't「悪い」ことを起こさせたい)、明示的なスリープでコードを「ジッター」することができます:

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

関数のインクリメントとデクリメントがエラーなしで実行されますか?

Python 'c'という名前のグローバル変数を使用することを明示的に指示する必要があるため、UnboundLocalErrorが発生するはずです。

したがって、インクリメント(デクリメントも)を次のように変更します。

def increment():
    global c
    c += 1

あなたのコードはスレッドセーフではないと思います。 この記事 Pythonのスレッド同期メカニズムについて役立つかもしれません。

0
ardsrk