実験用にスレッドセーフではないコードのチャンクを作成したいのですが、これらは2つのスレッドが呼び出す関数です。
c = 0
def increment():
c += 1
def decrement():
c -= 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
すべて複数のスレッドで共有されるリソース必須ロックが必要です。
いいえ、このコードは絶対に、明らかにスレッドセーフではありません。
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
(注:コードを機能させるには、各関数に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の他の実装では結果がまったく異なります。スレッドセーフが必要な場合は、ロックメカニズムが必要です。
確かに私はロックを使用することをお勧めします:
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
同期されたデコレータは、コードを読みやすくするのにも役立ちます。
コードがスレッドセーフではないであることを証明するのは簡単です。重要な部分でスリープを使用することにより、競合状態が発生する可能性を高めることができます(これは単に遅い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
あなたが実際にあなたのコードをnotスレッドセーフにしたいなら、そしてあなたが1万回(またはあなたが本当のときに1回)のように試みることなく実際に「悪い」ことが起こる可能性が高いならドン't「悪い」ことを起こさせたい)、明示的なスリープでコードを「ジッター」することができます:
def íncrement():
global c
x = c
from time import sleep
sleep(0.1)
c = x + 1
関数のインクリメントとデクリメントがエラーなしで実行されますか?
Python 'c'という名前のグローバル変数を使用することを明示的に指示する必要があるため、UnboundLocalErrorが発生するはずです。
したがって、インクリメント(デクリメントも)を次のように変更します。
def increment():
global c
c += 1
あなたのコードはスレッドセーフではないと思います。 この記事 Pythonのスレッド同期メカニズムについて役立つかもしれません。