Использование глобального словаря с потоками в Python

Является ли доступ / изменение значений словаря потокобезопасным?

У меня есть глобальный словарь foo и несколько потоков с идентификаторами id1, id2,..., idn, Это нормально для доступа и изменения fooзначения без выделения блокировки для него, если известно, что каждый поток будет работать только со своим значением, связанным с идентификатором, скажем, поток с id1 будет работать только с foo[id1]?

5 ответов

Решение

Предполагая CPython: да и нет. На самом деле безопасно извлекать / хранить значения из общего словаря в том смысле, что несколько одновременных запросов на чтение / запись не повредят словарь. Это связано с глобальной блокировкой интерпретатора ("GIL"), поддерживаемой реализацией. То есть:

Нить работает:

a = global_dict["foo"]

Резьба B работает:

global_dict["bar"] = "hello"

Поток C работает:

global_dict["baz"] = "world"

не повредит словарь, даже если все три попытки доступа происходят в одно и то же время. Интерпретатор будет сериализовать их каким-то неопределенным образом.

Тем не менее, результаты следующей последовательности не определены:

Тема А:

if "foo" not in global_dict:
   global_dict["foo"] = 1

Нить Б:

global_dict["foo"] = 2

поскольку тест / набор в потоке A не является атомарным (условие гонки "время проверки / время использования"). Так что, как правило, лучше, если вы заблокируете вещи:

from threading import RLock

lock = RLock()

def thread_A():
    lock.acquire()
    try:
        if "foo" not in global_dict:
            global_dict["foo"] = 1
    finally:
        lock.release()

def thread_B():
    lock.acquire()
    try:
        global_dict["foo"] = 2
    finally:
        lock.release()

Лучший, самый безопасный и портативный способ заставить каждый поток работать с независимыми данными:

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 для обеспечения выполнения только одного потока в виртуальной машине CPython одновременно. Это упрощает реализацию CPython, гарантируя, что никакие два процесса не могут одновременно обращаться к одной и той же памяти. Блокировка всего интерпретатора облегчает многопоточность интерпретатора за счет большей части параллелизма, обеспечиваемого многопроцессорными машинами. В прошлом были предприняты попытки создать интерпретатор с "свободной резьбой" (тот, который блокирует общие данные с более высокой степенью детализации), но до сих пор ни один из них не был успешным, потому что в обычном случае с одним процессором пострадала производительность.

Посмотрите, являются ли ненужные блокировки в многопоточном коде python-из-за-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 добавляет / обновляет пару ключ + значение в словаре. Итак, вы видите, что обновление словаря является атомарным и, следовательно, потокобезопасным.

Другие вопросы по тегам