web-dev-qa-db-ja.com

Pythonのスレッドでグローバル辞書を使用する

辞書値へのアクセス/変更はスレッドセーフですか?

グローバル辞書fooと、IDがid1id2、...、idnの複数のスレッドがあります。各スレッドがid関連の値でのみ機能することがわかっている場合、fooの値にアクセスして変更しても問題ありませんか。たとえば、id1のスレッドはfoo[id1]

40
Alex

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
59
Dirk

各スレッドが独立したデータを処理するための最良の最も安全で移植可能な方法は、次のとおりです。

import threading
tloc = threading.local()

これで、各スレッドは、グローバル名であっても、完全に独立したtlocオブジェクトを処理します。スレッドはtlocの属性を取得および設定でき、特に辞書が必要な場合はtloc.__dict__を使用できます。

スレッドのスレッドローカルストレージは、スレッドの最後でなくなります。スレッドに最終結果を記録させるには、終了する前にput結果をQueue.Queueの共通インスタンスに組み込みます(これは本質的にスレッドセーフです)。同様に、スレッドが処理するデータの初期値は、スレッドの開始時に渡される引数、またはQueueから取得される引数です。

アトミックに見える操作が実際にアトミックであることを期待するなど、その他の中途半端なアプローチは、Pythonの特定のバージョンおよびリリースで特定のケースで機能する場合がありますが、アップグレードまたはポートによって簡単に壊れる可能性があります。適切でクリーンで安全なアーキテクチャが配置が非常に簡単で、移植可能で、手軽で、高速である場合に、このような問題を危険にさらす本当の理由はありません。

27
Alex Martelli

似たようなものが必要だったので、ここに着陸しました。この短いスニペットであなたの答えを要約します:

_#!/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()を操作しながらロックを保持できます。

20
yota

[〜#〜] gil [〜#〜] は、たまたまCPythonを使用している場合に対応します。

グローバルインタープリターロック

Pythonスレッドが使用するロックは、一度に1つのスレッドのみがCPython仮想マシンで実行されることを保証します。これにより、2つのプロセスが同じメモリに同時にアクセスできないようにすることで、CPythonの実装が簡素化されます。インタプリタ全体をロックすると、マルチプロセッサマシンによって提供される並列処理の多くを犠牲にして、インタプリタをマルチスレッド化することが容易になります。これまで、「フリースレッド」インタプリタ(共有データをより細かい単位でロックするもの)ですが、シングルプロセッサの一般的なケースではパフォーマンスが低下するため、これまでのところ成功していません。

are-locks-unnecessary-in-multi-threaded-python-code-because-of-the-gil を参照してください。

6
gimel

使い方?:

>>> 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 命令はディクショナリのキー+値のペアを追加/更新します。したがって、ディクショナリの更新はアトミックであり、スレッドセーフであることがわかります。

0
Jatin Kumar