Документация Python неофициальный перевод
Содержание страницы

Гарантии потокобезопасностиThread Safety Guarantees

На этой странице описаны гарантии потокобезопасности для встроенных типов в сборке Python со свободной потоковой обработкой (free-threaded build). Описанные здесь гарантии действуют при использовании Python с отключённым GIL (режим свободной потоковой обработки). При включённом GIL большинство операций неявно сериализуются.

Общие рекомендации по написанию потокобезопасного кода в свободно-поточной версии Python см. в разделе Поддержка свободной потоковой обработки в Python.

Уровни потокобезопасностиThread safety levels

В документации C API используются следующие уровни для описания гарантий потокобезопасности каждой функции. Уровни перечислены от наименее к наиболее безопасному.

НесовместимыйIncompatible

Функция или операция, которую невозможно сделать безопасной для одновременного использования даже с внешней синхронизацией. Несовместимый код обычно обращается к глобальному состоянию несинхронизированным образом и должен вызываться только из одного потока в течение всего времени работы программы.

Пример: функция, которая изменяет общепроцессное состояние, например обработчики сигналов или переменные окружения, где параллельные вызовы из любых потоков, даже с внешней блокировкой, могут конфликтовать со средой выполнения или другими библиотеками.

СовместимыйCompatible

Функция или операция, которую можно безопасно вызывать из нескольких потоков при условии, что вызывающая сторона обеспечивает соответствующую внешнюю синхронизацию, например, удерживая блокировку на время каждого вызова. Без такой синхронизации параллельные вызовы могут привести к состояниям гонки или гонкам данных.

Пример: функция, которая читает или записывает данные объекта, внутреннее состояние которого не защищено блокировкой. Вызывающая сторона должна гарантировать, что никакие два потока не обращаются к одному и тому же объекту одновременно.

Безопасный на разных объектахSafe on distinct objects

Функция или операция, которую можно безопасно вызывать из нескольких потоков без внешней синхронизации, при условии, что каждый поток работает с разным объектом. Два потока могут вызывать функцию одновременно, но они не должны передавать один и тот же объект (или объекты, разделяющие общее внутреннее состояние) в качестве аргументов.

Пример: функция, которая изменяет поля структуры с помощью неатомарных записей. Два потока могут безопасно вызывать функцию каждый на своём экземпляре структуры, но параллельные вызовы на одном и том же экземпляре требуют внешней синхронизации.

Безопасный на общих объектахSafe on shared objects

Функция или операция, безопасная для одновременного использования на одном и том же объекте. Реализация использует внутреннюю синхронизацию (например, поблочные блокировки или критические секции) для защиты общего изменяемого состояния, поэтому вызывающей стороне не нужно предоставлять собственную блокировку.

Пример: PyList_GetItemRef() можно вызывать из нескольких потоков на одном и том же PyListObject – он использует внутреннюю синхронизацию для сериализации доступа.

АтомарныйAtomic

Функция или операция, которая выглядит атомарной по отношению к другим потокам – она выполняется мгновенно с точки зрения других потоков. Это самая сильная форма потокобезопасности.

Пример: PyMutex_IsLocked() выполняет атомарное чтение состояния мьютекса и может вызываться из любого потока в любое время.

Потокобезопасность для объектов спискаThread safety for list objects

Чтение одного элемента из list является атомарным:

lst[i]   # list.__getitem__

Следующие методы обходят список и используют атомарные чтения каждого элемента для выполнения своей функции. Это означает, что они могут возвращать результаты, на которые повлияли параллельные изменения:

item in lst
lst.index(item)
lst.count(item)

Все перечисленные выше операции не приобретают поблочных блокировок. Они не блокируют параллельные изменения. Другие операции, удерживающие блокировку, не помешают этим операциям наблюдать промежуточные состояния.

Все остальные операции, начиная с этого места, блокируются с помощью поблочной блокировки.

Запись одного элемента через lst[i] = x безопасна для вызова из нескольких потоков и не повредит список.

Следующие операции возвращают новые объекты и выглядят атомарными для других потоков:

lst1 + lst2    # объединяет два списка в новый список
x * lst        # повторяет lst x раз в новом списке
lst.copy()     # возвращает поверхностную копию списка

Следующие методы, которые работают только с одним элементом без необходимости сдвига, являются атомарными:

lst.append(x)  # добавить в конец списка, сдвиг не требуется
lst.pop()      # извлечь элемент из конца списка, сдвиг не требуется

Метод clear() также является атомарным. Другие потоки не могут наблюдать удаление элементов.

Метод sort() не является атомарным. Другие потоки не могут наблюдать промежуточные состояния во время сортировки, но список выглядит пустым на время сортировки.

Следующие операции могут позволить безблокировочным операциям наблюдать промежуточные состояния, поскольку они изменяют несколько элементов на месте:

lst.insert(idx, item)  # сдвигает элементы
lst.pop(idx)           # idx не в конце списка, сдвигает элементы
lst *= x               # копирует элементы на месте

Метод remove() может допускать параллельные изменения, поскольку сравнение элементов может выполнять произвольный код Python (через __eq__()).

extend() безопасно вызывать из нескольких потоков. Однако его гарантии зависят от переданного итерируемого объекта. Если это list, tuple, set, frozenset, dict или объект представления словаря (но не их подклассы), то операция extend безопасна при параллельных изменениях итерируемого объекта. В противном случае создаётся итератор, который может быть параллельно изменён другим потоком. То же самое относится к конкатенации списка с другими итерируемыми объектами на месте при использовании lst += iterable.

Аналогично, присваивание срезу списка с помощью lst[i:j] = iterable безопасно вызывать из нескольких потоков, но iterable блокируется только в том случае, если он также является list (но не его подклассом).

Операции, включающие множественные обращения, а также итерацию, никогда не являются атомарными. Например:

# НЕ атомарно: чтение-изменение-запись
lst[i] = lst[i] + 1

# НЕ атомарно: проверка-затем-действие
if lst:
    item = lst.pop()

# НЕ потокобезопасно: итерация во время изменения
for item in lst:
    process(item)  # другой поток может изменить lst

Рассмотрите внешнюю синхронизацию при совместном использовании экземпляров list между потоками.

Потокобезопасность объектов dictThread safety for dict objects

Создание словаря с помощью конструктора dict является атомарным, когда его аргумент – это dict или tuple. При использовании метода dict.fromkeys() создание словаря атомарно, когда аргумент – dict, tuple, set или frozenset.

Следующие операции и функции являются безблокировочными и атомарными.

d[key]       # dict.__getitem__
d.get(key)   # dict.get
key in d     # dict.__contains__
len(d)       # dict.__len__

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

Запись или удаление одного элемента безопасно вызывать из нескольких потоков, и это не повредит словарь:

d[key] = value        # записать
del d[key]            # удалить
d.pop(key)            # удалить и вернуть
d.popitem()           # удалить и вернуть последний элемент
d.setdefault(key, v)  # вставить, если отсутствует

Эти операции могут сравнивать ключи с помощью __eq__(), который может выполнять произвольный код Python. Во время таких сравнений словарь может быть изменён другим потоком. Для встроенных типов, таких как str, int и float, которые реализуют __eq__() на C, базовая блокировка не освобождается во время сравнений, и это не вызывает проблем.

Следующие операции возвращают новые объекты и удерживают блокировку объекта на время выполнения операции:

d.copy()      # возвращает поверхностную копию словаря
d | other     # объединяет два словаря в новый словарь
d.keys()      # возвращает новый объект представления dict_keys
d.values()    # возвращает новый объект представления dict_values
d.items()     # возвращает новый объект представления dict_items

Метод clear() удерживает блокировку на всё время своего выполнения. Другие потоки не могут наблюдать удаление элементов.

Следующие операции блокируют оба словаря. Для update() и |= это применимо только в том случае, если другой операнд является dict, который использует стандартный итератор dict (но не подклассы, переопределяющие итерацию). Для сравнения на равенство это относится к dict и его подклассам:

d.update(other_dict)  # оба заблокированы, если other_dict – это dict
d |= other_dict       # оба заблокированы, если other_dict – это dict
d == other_dict       # оба заблокированы для dict и его подклассов

Все операции сравнения также сравнивают значения с помощью __eq__(), поэтому для невстроенных типов блокировка может быть освобождена во время сравнения.

fromkeys() блокирует как новый словарь, так и итерируемый объект, если итерируемый объект является именно dict, set или frozenset (не подклассами):

dict.fromkeys(a_dict)      # блокирует оба
dict.fromkeys(a_set)       # блокирует оба
dict.fromkeys(a_frozenset) # блокирует оба

При обновлении из итерируемого объекта, не являющегося словарём, блокируется только целевой словарь. Итерируемый объект может быть параллельно изменён другим потоком:

d.update(iterable)        # iterable не является dict: заблокирован только d
d |= iterable             # iterable не является dict: заблокирован только d
dict.fromkeys(iterable)   # iterable не является dict/set/frozenset: заблокирован только результат

Операции, включающие множественные обращения, а также итерации, никогда не являются атомарными:

# НЕ атомарно: чтение-изменение-запись
d[key] = d[key] + 1

# НЕ атомарно: проверка-затем-действие (TOCTOU)
if key in d:
    del d[key]

# НЕ потокобезопасно: итерация во время изменения
for key, value in d.items():
    process(key)  # другой поток может изменить d

Чтобы избежать проблем TOCTOU (проверка-в-один-момент, использование-в-другой), используйте атомарные операции или обрабатывайте исключения:

# Используйте pop() со значением по умолчанию вместо проверки с последующим удалением
d.pop(key, None)

# Или обработайте исключение
try:
    del d[key]
except KeyError:
    pass

Для безопасной итерации по словарю, который может быть изменён другим потоком, итерируйте по копии:

# Сделайте копию для безопасной итерации
for key, value in d.copy().items():
    process(key)

Рассмотрите внешнюю синхронизацию при совместном использовании экземпляров dict между потоками.

Потокобезопасность объектов setThread safety for set objects

Функция len() не использует блокировки и является атомарной.

Следующая операция чтения является безблокировочной. Она не блокирует параллельные изменения и может наблюдать промежуточные состояния операций, которые удерживают блокировку объекта:

elem in s    # set.__contains__

Эта операция может сравнивать элементы с помощью __eq__(), который может выполнять произвольный код Python. Во время таких сравнений множество может быть изменено другим потоком. Для встроенных типов, таких как str, int и float, __eq__() не освобождает базовую блокировку во время сравнений, и это не вызывает проблем.

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

Добавление или удаление одного элемента безопасно вызывать из нескольких потоков и не повредит множество:

s.add(elem)      # добавляет элемент
s.remove(elem)   # удаляет элемент, вызывает исключение, если отсутствует
s.discard(elem)  # удаляет элемент, если присутствует
s.pop()          # удаляет и возвращает произвольный элемент

Эти операции также сравнивают элементы, поэтому применимы те же соображения относительно __eq__(), что и выше.

Метод copy() возвращает новый объект и удерживает блокировку объекта на всё время выполнения, так что он всегда атомарен.

Метод clear() удерживает блокировку на всё время своего выполнения. Другие потоки не могут наблюдать удаление элементов.

Следующие операции принимают только set или frozenset в качестве операндов и всегда блокируют оба объекта:

s |= other                   # other должен быть set/frozenset
s &= other                   # other должен быть set/frozenset
s -= other                   # other должен быть set/frozenset
s ^= other                   # other должен быть set/frozenset
s & other                    # other должен быть set/frozenset
s | other                    # other должен быть set/frozenset
s - other                    # other должен быть set/frozenset
s ^ other                    # other должен быть set/frozenset

set.update(), set.union(), set.intersection() и set.difference() могут принимать несколько итерируемых объектов в качестве аргументов. Все они перебирают все переданные итерируемые объекты и выполняют следующее:

set.symmetric_difference() пытается заблокировать оба объекта.

У обновлённых вариантов перечисленных выше методов также есть некоторые различия между ними:

Следующие методы всегда пытаются заблокировать оба объекта:

s.isdisjoint(other)          # оба заблокированы
s.issubset(other)            # оба заблокированы
s.issuperset(other)          # оба заблокированы

Операции, включающие множественные обращения, а также итерации, никогда не являются атомарными:

# НЕ атомарно: проверка-затем-действие
if elem in s:
      s.remove(elem)

# НЕ потокобезопасно: итерация во время изменения
for elem in s:
      process(elem)  # другой поток может изменить s

Рекомендуется использовать внешнюю синхронизацию при совместном использовании экземпляров set между потоками. Подробнее см. Python support for free threading.

Потокобезопасность объектов bytearrayThread safety for bytearray objects

Функция len() не использует блокировки и является атомарной.

Конкатенация и сравнения используют буферный протокол, который предотвращает изменение размера, но не удерживает блокировку для каждого объекта. Эти операции могут наблюдать промежуточные состояния параллельных изменений:

ba + other    # может наблюдать конкурентные записи
ba == other   # может наблюдать конкурентные записи
ba < other    # может наблюдать конкурентные записи

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

Чтение одного элемента или среза безопасно вызывать из нескольких потоков:

ba[i]        # bytearray.__getitem__
ba[i:j]      # срез

Следующие операции безопасно вызывать из нескольких потоков, и они не повредят bytearray:

ba[i] = x         # запись одного байта
ba[i:j] = values  # запись среза
ba.append(x)      # добавление одного байта
ba.extend(other)  # расширение итерируемым объектом
ba.insert(i, x)   # вставка одного байта
ba.pop()          # удаление и возврат последнего байта
ba.pop(i)         # удаление и возврат байта по индексу
ba.remove(x)      # удаление первого вхождения
ba.reverse()      # обращение на месте
ba.clear()        # удаление всех байтов

Присваивание среза блокирует оба объекта, когда значения является bytearray:

ba[i:j] = other_bytearray  # оба заблокированы

Следующие операции возвращают новые объекты и удерживают блокировку на объект на всё время выполнения:

ba.copy()     # возвращает поверхностную копию
ba * n        # повтор в новый bytearray

Проверка принадлежности удерживает блокировку на всё время выполнения:

x in ba       # bytearray.__contains__

Все остальные методы bytearray (такие как find(), replace(), split(), decode() и т.д.) удерживают блокировку на объект на всё время своего выполнения.

Операции, включающие множественные обращения, а также итерации, никогда не являются атомарными:

# НЕ атомарно: проверка-затем-действие
if x in ba:
    ba.remove(x)

# НЕ потокобезопасно: итерация во время изменения
for byte in ba:
    process(byte)  # другой поток может изменить ba

Для безопасной итерации по bytearray, который может быть изменён другим потоком, выполняйте итерацию по копии:

# Сделайте копию для безопасной итерации
for byte in ba.copy():
    process(byte)

Рекомендуется использовать внешнюю синхронизацию при совместном использовании экземпляров bytearray между потоками. Подробнее см. Python support for free threading.

Потокобезопасность объектов memoryviewThread safety for memoryview objects

Объекты memoryview предоставляют доступ к внутренним данным базового объекта без копирования. Потокобезопасность зависит как от самого memoryview, так и от экспортёра буфера базового объекта.

Реализация memoryview использует атомарные операции для отслеживания собственных экспортов в сборке со свободной многопоточностью. Создание и освобождение memoryview потокобезопасны. Доступ к атрибутам (например, shape, format) читает поля, которые неизменны на время существования memoryview, поэтому одновременное чтение безопасно, пока memoryview не был освобождён.

Однако фактические данные, доступ к которым осуществляется через memoryview, принадлежат базовому объекту. Одновременный доступ к этим данным безопасен только в том случае, если базовый объект это поддерживает:

  • Для неизменяемых объектов, таких как bytes, одновременное чтение через несколько memoryview безопасно.

  • Для изменяемых объектов, таких как bytearray, чтение и запись одной и той же области памяти из нескольких потоков без внешней синхронизации небезопасны и могут привести к повреждению данных. Обратите внимание, что даже memoryviews, доступные только для чтения, изменяемых объектов не предотвращают гонку данных, если базовый объект изменяется из другого потока.

# НЕ безопасно: конкурентные записи в один буфер
data = bytearray(1000)
view = memoryview(data)
# Поток 1: view[0:500] = b'x' * 500
# Поток 2: view[0:500] = b'y' * 500
# Безопасно: используется блокировка для конкурентного доступа
import threading
lock = threading.Lock()
data = bytearray(1000)
view = memoryview(data)

with lock:
    view[0:500] = b'x' * 500

Изменение размера или перераспределение базового объекта (например, вызов bytearray.resize()) во время экспорта memoryview вызывает исключение BufferError. Это действует независимо от потоков.