Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

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

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

a = global_dict["foo"]

Работа с потоком B:

global_dict["bar"] = "hello"

Работа с потоком C:

global_dict["baz"] = "world"

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

Однако результаты следующей последовательности undefined:

Тема А:

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

Тема B:

global_dict["foo"] = 2

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

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()

Ответ 2

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

import threading
tloc = threading.local()

Теперь каждый поток работает с полностью независимым объектом tloc, хотя это глобальное имя. Поток может получать и устанавливать атрибуты на tloc, использовать tloc.__dict__, если он специально нуждается в словаре и т.д.

Потоковое локальное хранилище для потока удаляется в конце потока; чтобы потоки записывали свои окончательные результаты, пусть они put их результаты, прежде чем они завершатся, в общий экземпляр Queue.Queue (который по сути является потокобезопасным). Аналогичным образом, начальными значениями для потока данных, на которые должен работать поток, могут быть аргументы, переданные при запуске потока, или взятые из Queue.

Другие наполовину искушенные подходы, например, надеясь, что операции, которые кажутся атомными, действительно являются атомарными, могут работать для конкретных случаев в данной версии и выпуске Python, но могут легко сломаться обновлениями или портами. Нет реальной причины рисковать такими проблемами, когда правильную, чистую и безопасную архитектуру так легко организовать, переносить, удобно и быстро.

Ответ 3

Поскольку мне было нужно что-то подобное, я приземлился здесь. Я суммирую ваши ответы в этом коротком фрагменте:

#!/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()

Ответ 4

GIL позаботится об этом, если вы используете CPython.

блокировка глобального интерпретатора

Блокировка, используемая потоками Python, для обеспечения того, чтобы только один поток выполнялся на виртуальной машине CPython за раз. Это упрощает реализацию CPython, гарантируя, что никакие два процесса не могут одновременно обращаться к одной и той же памяти. Блокировка всего интерпретатора облегчает интерпретатор многопоточности за счет большей части parallelism, предоставляемой многопроцессорными машинами. В прошлом предпринимались усилия по созданию "свободнопотокового" интерпретатора (который блокирует общие данные с гораздо меньшей степенью детализации), но до сих пор ни один из них не был успешным, поскольку производительность была связана с общим однопроцессорным случаем.

См. are-locks-unnecessary-in-multi-threaded-python-code-because-of-the-gil.

Ответ 5

Как это работает?:

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