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

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

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

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

🤔 А знаете ли вы, что...
С Python можно создавать веб-скраперы для извлечения данных из веб-сайтов.


2
71
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[:] = () — у меня такое ощущение, что нужно дважды подумать, прежде чем заменять это вызовом метода, который может привести к нежелательным условиям гонки.