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