いいえ、このコードは絶対に、明らかにスレッドセーフではありません。
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インタープリターは、100オペコードごとにアクティブなスレッドを切り替えます(1つのスレッドからGILを解放して、別のスレッドがGILを使用できるようにします)。(これらは両方とも実装の詳細です。)競合状態は、ロードと保存の間に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