Является ли Python list.clear() потокобезопасным?

Предположим, что в Python один поток добавляет/извлекает элементы в/из аналогичного встроенного контейнера list/collections.deque/, в то время как другой поток время от времени очищает контейнер с помощью своего метода clear(). Является ли это взаимодействие потокобезопасным? Или может ли clear() помешать параллельной операции append()/pop(), оставив список неясным или поврежденным?

Моя интерпретация принятого ответа здесь предполагает, что GIL должен предотвращать такое вмешательство, по крайней мере, для списков. Я прав?

В качестве продолжения: если это не потокобезопасно, я полагаю, мне следует вместо этого использовать queue.Queue. Но каков наилучший (т. е. самый чистый, безопасный и быстрый) способ удалить его из второго потока? См. комментарии к этому ответу, чтобы узнать об опасениях по поводу использования (недокументированного) queue.Queue().queue.clear() метода. Действительно ли мне нужно использовать цикл для get() всех элементов один за другим?

AFAIK, все встроенные операции должны быть потокобезопасными из-за использования GIL.

Barmar 05.08.2024 19:46

Даже если метод clear был «потокобезопасным», как вы могли бы координировать активность потока, который «иногда очищает контейнер», в то время как «один поток добавляет/извлекает элементы», если вы не используете объекты Lock для остановки потоков? мешать друг другу? Даже если «clear» никогда не сможет испортить представление Python о том, как должен выглядеть список в целом, это не означает, что представление вашей программы о том, что должен содержать этот конкретный список, не будет искажено. Создание программы полностью из потокобезопасных объектов не гарантирует, что сама программа является «потокобезопасной».

Solomon Slow 05.08.2024 21:17

@SolomonSlow По общему признанию, я не предоставил много подробностей о моем конкретном варианте использования. Я добавляю в очередь (на самом деле я использую deque из-за его функции maxlen) в одном потоке и периодически обрабатываю и очищаю ее в другом потоке. Это правда, что мне не удастся обработать элементы, добавленные между обработкой и очисткой, но в моем случае это не проблема, и я не могу придумать никаких других проблем с этим подходом.

charlescochran 05.08.2024 21:25

@charlescochran, пожалуйста — мне пришлось существенно изменить ответ после того, как он был принят — убедитесь, что он все еще действителен для вас.

jsbueno 05.08.2024 23:43

@jsbueno Спасибо за обновление. Смотрите мой комментарий ниже.

charlescochran 06.08.2024 16:24
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
5
71
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Методы обновления, такие как 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 внутри)».

no comment 05.08.2024 22:08

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

jsbueno 05.08.2024 23:12

@jsbueno Чтобы расширить свой ответ, если вы запустите тот же тест со списком, инициализированным как target = ['old1', 'old2'] вместо target = [], вы получите тот же результат (['this tries to be first', 'thing']), что указывает на то, что target.clear() успешно удалил старые элементы. Конечно, вы вручную создали здесь условие гонки относительно того, в каком порядке вставляются два других элемента (и очищается ли первый), но я чувствую, что семантика clearing здесь вполне детерминирована, поэтому я думаю, что его можно безопасно использовать. . При этом я, вероятно, выберу замок для ясности.

charlescochran 06.08.2024 16:23

Да, существующие элементы в списке в точке вызова .clear() всегда удаляются: в какой-то момент перед возвратом функции длина списка устанавливается равной 0 — после этого что-то может произойти, и это усложняется. Простая «последовательность блокировки», которую я нацарапал с помощью collections.abc.MutableMapping, просто удаляла элемент, вставленный из параллельного потока во время «очистки» вызова :-( — такие вещи нужно делать осторожно.

jsbueno 06.08.2024 16:30

@jsbueno Под «отброшенным» вы имеете в виду, что конструктор элемента вообще никогда не вызывался (в отличие от простого удаления через ясное право после создания и вставки)? Да, это звучит сложно, и это объясняет, почему большая часть дискуссий по этому вопросу в SO выглядит довольно туманной.

charlescochran 06.08.2024 16:48

Я мог бы это понять — оказывается, что простого добавления блокировки к обязательным методам, указанным в документации, недостаточно: реализация collections.abc.MutableSequence этого класса будет выполнять один вызов clear для каждого элемента. Реализация __delitem__ решила эту проблему для элементов, поступающих из других потоков.

jsbueno 06.08.2024 17:02

Но для пограничного случая, когда элемент вставляется в побочный эффект __del__ - который происходит в том же потоке, который вызывается .clear(), этого недостаточно - и необходимо еще одно внутреннее состояние, помимо self.lock = threading.RLock(), если это должно быть покрыто, для пример.

jsbueno 06.08.2024 17:03

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