Предположим, что в Python один поток добавляет/извлекает элементы в/из аналогичного встроенного контейнера list
/collections.deque
/, в то время как другой поток время от времени очищает контейнер с помощью своего метода clear()
. Является ли это взаимодействие потокобезопасным? Или может ли clear()
помешать параллельной операции append()
/pop()
, оставив список неясным или поврежденным?
Моя интерпретация принятого ответа здесь предполагает, что GIL должен предотвращать такое вмешательство, по крайней мере, для списков. Я прав?
В качестве продолжения: если это не потокобезопасно, я полагаю, мне следует вместо этого использовать queue.Queue
. Но каков наилучший (т. е. самый чистый, безопасный и быстрый) способ удалить его из второго потока? См. комментарии к этому ответу, чтобы узнать об опасениях по поводу использования (недокументированного) queue.Queue().queue.clear()
метода. Действительно ли мне нужно использовать цикл для get()
всех элементов один за другим?
🤔 А знаете ли вы, что...
С Python можно создавать веб-скраперы для извлечения данных из веб-сайтов.
Методы обновления, такие как list.clear
, не являются атомарными в том смысле, что другие потоки могут добавлять элементы в список (или другой контейнер) до того, как метод вернется в текущий код. Они «потокобезопасны» в том смысле, что никогда не будут находиться в несогласованном состоянии, которое может вызвать исключение, но не «атомарное».
Другими словами: объект списка никогда не будет «сломан» с использованием блокировки или без него, но все, что находится внутри него, не является детерминированным.
Следующий фрагмент вставляет данные до того, как list.clear()
вернет данные как в том же потоке, так и из другого потока:
import threading, time
class A:
def __init__(self, container, delay=0.2):
self.container = container
self.delay = delay
def __del__(self):
time.sleep(self.delay)
self.container.append("thing")
def doit():
target = []
def interferer():
time.sleep(0.1)
target.append("this tries to be first")
target.append(A(target, delay=0.3))
t = threading.Thread(target=interferer)
t.start()
target.clear()
return target
In [37]: doit()
Out[37]: ['this tries to be first', 'thing']
Итак, если нужна «потокобезопасная» и «атомарная» последовательность — ее нужно создать из collections.abc.MutableSequence
и соответствующих блокировок в методах, выполняющих мутации.
оригинальный ответ
Как сказано в комментариях: все операции над встроенными структурами данных в Python являются потокобезопасными - что до сих пор обеспечивает это, так это GIL (глобальная блокировка интерпретатора), которая в противном случае наказывает многопоточный код в Python.
Для Python3.13 и более поздних версий будет возможность запуска кода Python без GIL, но это гарантия языка, что такие операции со встроенными структурами данных останутся потокобезопасными за счет использования более мелкозернистой блокировки — проверьте Сеанс безопасности потоков контейнеров на PEP 703 (поскольку он не только объясняет механизм вперед, но и подтверждает текущий статус-кво этих модификаций, которые эффективно атомарно безопасны, хотя и не «атомарны»)
Однако, в зависимости от имеющегося у вас кода, вы можете захотеть выразить модификацию списка с помощью другой операции вместо вызова метода, поскольку некоторые методы не могут быть атомарными. В связанном сеансе PEP 703 выше приведен пример list.extend
, который при использовании с объектом-генератором просто не может быть атомарным. Поэтому, чтобы уменьшить вероятность того, что кто-то изменит ваш код в будущем, очистку списка можно выразить с помощью mylist[:] = ()
— у меня такое ощущение, что нужно дважды подумать, прежде чем заменять это вызовом метода, который может привести к нежелательным условиям гонки.