辞書値へのアクセス/変更はスレッドセーフですか?
グローバル辞書foo
と、IDがid1
、id2
、...、idn
の複数のスレッドがあります。各スレッドがid関連の値でのみ機能することがわかっている場合、foo
の値にアクセスして変更しても問題ありませんか。たとえば、id1
のスレッドはfoo[id1]
?
CPythonを想定:はい、いいえ。複数の同時読み取り/書き込み要求によって辞書が破損しないという意味で、共有辞書から値をフェッチ/保存しても実際には安全です。これは、実装によって維持されるグローバルインタープリターロック( "GIL")が原因です。あれは:
実行中のスレッドA:
a = global_dict["foo"]
実行中のスレッドB:
global_dict["bar"] = "hello"
実行中のスレッドC:
global_dict["baz"] = "world"
3つのアクセス試行がすべて「同時に」発生しても、辞書は破損しません。インタープリターは、未定義の方法でそれらをシリアライズします。
ただし、次のシーケンスの結果は未定義です。
スレッドA:
if "foo" not in global_dict:
global_dict["foo"] = 1
スレッドB:
global_dict["foo"] = 2
スレッドAのテスト/セットはアトミックではないため(「チェック時間/使用時間」の競合状態)。ですから、 lock things :
from threading import RLock
lock = RLock()
def thread_A():
with lock:
if "foo" not in global_dict:
global_dict["foo"] = 1
def thread_B():
with lock:
global_dict["foo"] = 2
各スレッドが独立したデータを処理するための最良の最も安全で移植可能な方法は、次のとおりです。
import threading
tloc = threading.local()
これで、各スレッドは、グローバル名であっても、完全に独立したtloc
オブジェクトを処理します。スレッドはtloc
の属性を取得および設定でき、特に辞書が必要な場合はtloc.__dict__
を使用できます。
スレッドのスレッドローカルストレージは、スレッドの最後でなくなります。スレッドに最終結果を記録させるには、終了する前にput
結果をQueue.Queue
の共通インスタンスに組み込みます(これは本質的にスレッドセーフです)。同様に、スレッドが処理するデータの初期値は、スレッドの開始時に渡される引数、またはQueue
から取得される引数です。
アトミックに見える操作が実際にアトミックであることを期待するなど、その他の中途半端なアプローチは、Pythonの特定のバージョンおよびリリースで特定のケースで機能する場合がありますが、アップグレードまたはポートによって簡単に壊れる可能性があります。適切でクリーンで安全なアーキテクチャが配置が非常に簡単で、移植可能で、手軽で、高速である場合に、このような問題を危険にさらす本当の理由はありません。
似たようなものが必要だったので、ここに着陸しました。この短いスニペットであなたの答えを要約します:
_#!/usr/bin/env python3
import threading
class ThreadSafeDict(dict) :
def __init__(self, * p_arg, ** n_arg) :
dict.__init__(self, * p_arg, ** n_arg)
self._lock = threading.Lock()
def __enter__(self) :
self._lock.acquire()
return self
def __exit__(self, type, value, traceback) :
self._lock.release()
if __name__ == '__main__' :
u = ThreadSafeDict()
with u as m :
m[1] = 'foo'
print(u)
_
そのため、with
構文を使用して、dict()
を操作しながらロックを保持できます。
[〜#〜] gil [〜#〜] は、たまたまCPython
を使用している場合に対応します。
グローバルインタープリターロック
Pythonスレッドが使用するロックは、一度に1つのスレッドのみがCPython仮想マシンで実行されることを保証します。これにより、2つのプロセスが同じメモリに同時にアクセスできないようにすることで、CPythonの実装が簡素化されます。インタプリタ全体をロックすると、マルチプロセッサマシンによって提供される並列処理の多くを犠牲にして、インタプリタをマルチスレッド化することが容易になります。これまで、「フリースレッド」インタプリタ(共有データをより細かい単位でロックするもの)ですが、シングルプロセッサの一般的なケースではパフォーマンスが低下するため、これまでのところ成功していません。
are-locks-unnecessary-in-multi-threaded-python-code-because-of-the-gil を参照してください。
使い方?:
>>> import dis
>>> demo = {}
>>> def set_dict():
... demo['name'] = 'Jatin Kumar'
...
>>> dis.dis(set_dict)
2 0 LOAD_CONST 1 ('Jatin Kumar')
3 LOAD_GLOBAL 0 (demo)
6 LOAD_CONST 2 ('name')
9 STORE_SUBSCR
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
上記の各命令はGILロックホールドで実行され、 STORE_SUBSCR 命令はディクショナリのキー+値のペアを追加/更新します。したがって、ディクショナリの更新はアトミックであり、スレッドセーフであることがわかります。