Содержание страницы
Гарантии потокобезопасности¶Thread Safety Guarantees
На этой странице описаны гарантии потокобезопасности для встроенных типов в сборке Python со свободной потоковой обработкой (free-threaded build). Описанные здесь гарантии действуют при использовании Python с отключённым GIL (режим свободной потоковой обработки). При включённом GIL большинство операций неявно сериализуются.
Общие рекомендации по написанию потокобезопасного кода в свободно-поточной версии Python см. в разделе Поддержка свободной потоковой обработки в Python.
Уровни потокобезопасности¶Thread safety levels
В документации C API используются следующие уровни для описания гарантий потокобезопасности каждой функции. Уровни перечислены от наименее к наиболее безопасному.
Несовместимый¶Incompatible
Функция или операция, которую невозможно сделать безопасной для одновременного использования даже с внешней синхронизацией. Несовместимый код обычно обращается к глобальному состоянию несинхронизированным образом и должен вызываться только из одного потока в течение всего времени работы программы.
Пример: функция, которая изменяет общепроцессное состояние, например обработчики сигналов или переменные окружения, где параллельные вызовы из любых потоков, даже с внешней блокировкой, могут конфликтовать со средой выполнения или другими библиотеками.
Совместимый¶Compatible
Функция или операция, которую можно безопасно вызывать из нескольких потоков при условии, что вызывающая сторона обеспечивает соответствующую внешнюю синхронизацию, например, удерживая блокировку на время каждого вызова. Без такой синхронизации параллельные вызовы могут привести к состояниям гонки или гонкам данных.
Пример: функция, которая читает или записывает данные объекта, внутреннее состояние которого не защищено блокировкой. Вызывающая сторона должна гарантировать, что никакие два потока не обращаются к одному и тому же объекту одновременно.
Безопасный на разных объектах¶Safe on distinct objects
Функция или операция, которую можно безопасно вызывать из нескольких потоков без внешней синхронизации, при условии, что каждый поток работает с разным объектом. Два потока могут вызывать функцию одновременно, но они не должны передавать один и тот же объект (или объекты, разделяющие общее внутреннее состояние) в качестве аргументов.
Пример: функция, которая изменяет поля структуры с помощью неатомарных записей. Два потока могут безопасно вызывать функцию каждый на своём экземпляре структуры, но параллельные вызовы на одном и том же экземпляре требуют внешней синхронизации.
Атомарный¶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 между потоками.
Потокобезопасность объектов dict¶Thread 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 между потоками.
Потокобезопасность объектов set¶Thread 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.update()иset.union()блокируют оба объекта только когда
set.intersection()иset.difference()всегда пытаются заблокироватьвсе объекты.
set.symmetric_difference() пытается заблокировать оба объекта.
У обновлённых вариантов перечисленных выше методов также есть некоторые различия между ними:
set.difference_update()иset.intersection_update()пытаютсяблокировать все объекты по одному.
set.symmetric_difference_update()блокирует аргументы только если они являются
Следующие методы всегда пытаются заблокировать оба объекта:
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.
Потокобезопасность объектов bytearray¶Thread 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.
Потокобезопасность объектов memoryview¶Thread 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. Это действует независимо от потоков.