Предположим, что в Python один поток добавляет/извлекает элементы в/из аналогичного встроенного контейнера list
/collections.deque
/, в то время как другой поток время от времени очищает контейнер с помощью своего метода clear()
. Является ли это взаимодействие потокобезопасным? Или может ли clear()
помешать параллельной операции append()
/pop()
, оставив список неясным или поврежденным?
Моя интерпретация принятого ответа здесь предполагает, что GIL должен предотвращать такое вмешательство, по крайней мере, для списков. Я прав?
В качестве продолжения: если это не потокобезопасно, я полагаю, мне следует вместо этого использовать queue.Queue
. Но каков наилучший (т. е. самый чистый, безопасный и быстрый) способ удалить его из второго потока? См. комментарии к этому ответу, чтобы узнать об опасениях по поводу использования (недокументированного) queue.Queue().queue.clear()
метода. Действительно ли мне нужно использовать цикл для get()
всех элементов один за другим?
Даже если метод clear
был «потокобезопасным», как вы могли бы координировать активность потока, который «иногда очищает контейнер», в то время как «один поток добавляет/извлекает элементы», если вы не используете объекты Lock
для остановки потоков? мешать друг другу? Даже если «clear» никогда не сможет испортить представление Python о том, как должен выглядеть список в целом, это не означает, что представление вашей программы о том, что должен содержать этот конкретный список, не будет искажено. Создание программы полностью из потокобезопасных объектов не гарантирует, что сама программа является «потокобезопасной».
@SolomonSlow По общему признанию, я не предоставил много подробностей о моем конкретном варианте использования. Я добавляю в очередь (на самом деле я использую deque
из-за его функции maxlen
) в одном потоке и периодически обрабатываю и очищаю ее в другом потоке. Это правда, что мне не удастся обработать элементы, добавленные между обработкой и очисткой, но в моем случае это не проблема, и я не могу придумать никаких других проблем с этим подходом.
@charlescochran, пожалуйста — мне пришлось существенно изменить ответ после того, как он был принят — убедитесь, что он все еще действителен для вас.
@jsbueno Спасибо за обновление. Смотрите мой комментарий ниже.
Методы обновления, такие как 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[:] = ()
— у меня такое ощущение, что нужно дважды подумать, прежде чем заменять это вызовом метода, который может привести к нежелательным условиям гонки.
Разве в PEP на самом деле не говорится, что операции не гарантированно будут атомарными? Цитата: «GIL не обязательно гарантирует, что операции являются атомарными или остаются корректными, когда несколько операций выполняются одновременно. Например, list.extend(iterable) может не выглядеть атомарным, если итератор имеет итератор, реализованный в Python (или высвобождает GIL внутри себя). )" Точно так же clear()
может удалить элемент, который не имеет других ссылок, чтобы его __del__
вызывался и который также мог быть «реализован в Python (или освобождал GIL внутри)».
Действительно - очистка и иное удаление списка не являются "атомарными" - языковая гарантия заключается в том, что объект списка не будет поврежден - и что они потокобезопасны - но синхронные инициируемые события в том же потоке могут изменить это.
@jsbueno Чтобы расширить свой ответ, если вы запустите тот же тест со списком, инициализированным как target = ['old1', 'old2']
вместо target = []
, вы получите тот же результат (['this tries to be first', 'thing']
), что указывает на то, что target.clear()
успешно удалил старые элементы. Конечно, вы вручную создали здесь условие гонки относительно того, в каком порядке вставляются два других элемента (и очищается ли первый), но я чувствую, что семантика clearing
здесь вполне детерминирована, поэтому я думаю, что его можно безопасно использовать. . При этом я, вероятно, выберу замок для ясности.
Да, существующие элементы в списке в точке вызова .clear()
всегда удаляются: в какой-то момент перед возвратом функции длина списка устанавливается равной 0 — после этого что-то может произойти, и это усложняется. Простая «последовательность блокировки», которую я нацарапал с помощью collections.abc.MutableMapping
, просто удаляла элемент, вставленный из параллельного потока во время «очистки» вызова :-( — такие вещи нужно делать осторожно.
@jsbueno Под «отброшенным» вы имеете в виду, что конструктор элемента вообще никогда не вызывался (в отличие от простого удаления через ясное право после создания и вставки)? Да, это звучит сложно, и это объясняет, почему большая часть дискуссий по этому вопросу в SO выглядит довольно туманной.
Я мог бы это понять — оказывается, что простого добавления блокировки к обязательным методам, указанным в документации, недостаточно: реализация collections.abc.MutableSequence
этого класса будет выполнять один вызов clear
для каждого элемента. Реализация __delitem__
решила эту проблему для элементов, поступающих из других потоков.
Но для пограничного случая, когда элемент вставляется в побочный эффект __del__
- который происходит в том же потоке, который вызывается .clear()
, этого недостаточно - и необходимо еще одно внутреннее состояние, помимо self.lock = threading.RLock()
, если это должно быть покрыто, для пример.
AFAIK, все встроенные операции должны быть потокобезопасными из-за использования GIL.