Pyhtonのマルチスレッド周りで実験をしていたところ,
Python3.10より動作が変わった?点があったのでメモしておきます。
注意: 私の環境での結果をメモしただけです。
ソースレベルでの調査やリリースノートの確認はしていません。
詳しい情報があれば教えてください
後日調査して内容をアップグレードする予定です.(時間があれば)
大前提
PythonはGILがありマルチスレッドで実行しても,
GILにより実行されるスレッド数は常に1スレッドに限定されます.
ただし, 各動作がアトミックに行われるわけではないため, スレッドセーフではありません.
意図せぬ競合を起こさないため, クリティカルセッションでは必ずWith構文を用いたロックをしましょう
本題
GILの確認のためマルチスレッド処理を実験してみたところ,
Python3.9.6とPython3.10.0で動作が異なったのでメモしておきます.
環境
- Ubuntu 64Bit(x86-64), RaspberryPiOS 64bit(Aarch64),
- Python3.10 (64bit)
- Python3.9.6 (64bit)
競合状態を起こすために以下のコードで実験しました。
クラス変数&ThreadPoolExecutorバージョン
from concurrent.futures import (ThreadPoolExecutor, wait)
class Counter:
def __init__(self):
self.count = 0
def countup(self):
for _ in range(100000):
self.count += 1
def main():
counter = Counter()
with ThreadPoolExecutor() as executor:
features = [executor.submit(counter.countup) for _ in range(10)]
print(counter.count)
if __name__ == '__main__':
main()
ついでにグローバル変数版の実験も行いました。
グローバル変数&threadingバージョン
import threading
sum = 0
def increment():
global sum
for _i in range(100000):
sum += 1
def main():
threads = []
for _i in range(10):
threads.append(threading.Thread(target=increment))
for i in range(10):
threads[i].start()
for i in range(10):
threads[i].join()
print(sum)
if __name__ == '__main__':
main()
もし上記コードでスレッド競合が起きない場合,
コンソールには1000000が表示されます.
こちらをPython3.9.6で5回実行したところ結果は
282579
533131
507843
455148
540169
となり, スレッド競合が発生していることがわかります.
これは,
def countup(self):
for _ in range(100000):
self.count += 1
で, self.countを読み取り1を追加する間に,
別スレッドがself.countを再度読み取るため発生する競合です.
一方, Python3.10で同じファイルを5回実行したところ,
1000000
1000000
1000000
1000000
1000000
という結果になりました.
あれ?競合が起きていない....
おそらくPython3.10より
self.count += 1
が完了するまでは, 別スレッドが動作しないためであると思われます.
結論
Pythonのマルチスレッドはスレッドセーフではありません.
今回のような処理ではPython3.10よりスレッドセーフに見えるというだけです.
マルチスレッドのクリティカルセクションはロックを忘れないようにしましょう.
感想
Python3.10で競合状態を起こすサンプルを書くのが難しくなりました...
マルチスレッドだと競合が発生するので、ちゃんとロック取りましょうというサンプルが簡単に書けなくなって逆に困った。
基本いいことなんでしょうけど、Python3.10で書いたコードがPython3.9以下では競合が発生するので注意が必要ですね。
Python3.10が普及すると,
Pythonがスレッドセーフでないことを示すサンプルがスレッドセーフな動作になりそうです...
その結果Pythonはスレッドセーフという誤解が発生しなければいいのですが...