Spec-Zone.ru › Python 3.14

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

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

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

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

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

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

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

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

Совместимый

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

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

Безопасный для разных объектов

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

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

Безопасный для общих объектов

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

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

Атомарный

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

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

Потокобезопасность объектов list

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

lst[i]   # list.__getitem__

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

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

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

Все остальные операции далее используют блокировку отдельного объекта.

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

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

lst1 + lst2    # concatenates two lists into a new list
x * lst        # repeats lst x times into a new list
lst.copy()     # returns a shallow copy of the list

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

lst.append(x)  # append to the end of the list, no shifting required
lst.pop()      # pop element from the end of the list, no shifting required

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

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

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

lst.insert(idx, item)  # shifts elements
lst.pop(idx)           # idx not at the end of the list, shifts elements
lst *= x               # copies elements in place

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

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

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

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

# NOT atomic: read-modify-write
lst[i] = lst[i] + 1

# NOT atomic: check-then-act
if lst:
    item = lst.pop()

# NOT thread-safe: iteration while modifying
for item in lst:
    process(item)  # another thread may modify lst

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

Потокобезопасность объектов dict

Создание словаря с помощью конструктора 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        # write
del d[key]            # delete
d.pop(key)            # remove and return
d.popitem()           # remove and return last item
d.setdefault(key, v)  # insert if missing

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

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

d.copy()      # returns a shallow copy of the dictionary
d | other     # merges two dicts into a new dict
d.keys()      # returns a new dict_keys view object
d.values()    # returns a new dict_values view object
d.items()     # returns a new dict_items view object

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

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

d.update(other_dict)  # both locked when other_dict is a dict
d |= other_dict       # both locked when other_dict is a dict
d == other_dict       # both locked for dict and subclasses

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

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

dict.fromkeys(a_dict)      # locks both
dict.fromkeys(a_set)       # locks both
dict.fromkeys(a_frozenset) # locks both

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

d.update(iterable)        # iterable is not a dict: only d locked
d |= iterable             # iterable is not a dict: only d locked
dict.fromkeys(iterable)   # iterable is not a dict/set/frozenset: only result locked

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

# NOT atomic: read-modify-write
d[key] = d[key] + 1

# NOT atomic: check-then-act (TOCTOU)
if key in d:
    del d[key]

# NOT thread-safe: iteration while modifying
for key, value in d.items():
    process(key)  # another thread may modify d

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

# Use pop() with default instead of check-then-delete
d.pop(key, None)

# Or handle the exception
try:
    del d[key]
except KeyError:
    pass

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

# Make a copy to iterate safely
for key, value in d.copy().items():
    process(key)

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

Потокобезопасность объектов set

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

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

elem in s    # set.__contains__

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

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

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

s.add(elem)      # add element
s.remove(elem)   # remove element, raise if missing
s.discard(elem)  # remove element if present
s.pop()          # remove and return arbitrary element

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

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

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

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

s |= other                   # other must be set/frozenset
s &= other                   # other must be set/frozenset
s -= other                   # other must be set/frozenset
s ^= other                   # other must be set/frozenset
s & other                    # other must be set/frozenset
s | other                    # other must be set/frozenset
s - other                    # other must be set/frozenset
s ^ other                    # other must be set/frozenset

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

  • set.update() and set.union() lock both objects only when

    другой операнд — set, frozenset или dict.

  • set.intersection() and set.difference() always try to lock

    любые объекты.

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

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

  • set.difference_update() and set.intersection_update() try

    блокировать все объекты по одному.

  • set.symmetric_difference_update() only locks the arguments if it is

    типа set, frozenset или dict.

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

s.isdisjoint(other)          # both locked
s.issubset(other)            # both locked
s.issuperset(other)          # both locked

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

# NOT atomic: check-then-act
if elem in s:
      s.remove(elem)

# NOT thread-safe: iteration while modifying
for elem in s:
      process(elem)  # another thread may modify s

При совместном использовании экземпляров set между потоками рассмотрите возможность внешней синхронизации. Дополнительную информацию см. в разделе Поддержка свободной многопоточности в Python.

Потокобезопасность объектов bytearray

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

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

ba + other    # may observe concurrent writes
ba == other   # may observe concurrent writes
ba < other    # may observe concurrent writes

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

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

ba[i]        # bytearray.__getitem__
ba[i:j]      # slice

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

ba[i] = x         # write single byte
ba[i:j] = values  # write slice
ba.append(x)      # append single byte
ba.extend(other)  # extend with iterable
ba.insert(i, x)   # insert single byte
ba.pop()          # remove and return last byte
ba.pop(i)         # remove and return byte at index
ba.remove(x)      # remove first occurrence
ba.reverse()      # reverse in place
ba.clear()        # remove all bytes

При присваивании срезу блокируются оба объекта, если values — это bytearray:

ba[i:j] = other_bytearray  # both locked

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

ba.copy()     # returns a shallow copy
ba * n        # repeat into new bytearray

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

x in ba       # bytearray.__contains__

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

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

# NOT atomic: check-then-act
if x in ba:
    ba.remove(x)

# NOT thread-safe: iteration while modifying
for byte in ba:
    process(byte)  # another thread may modify ba

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

# Make a copy to iterate safely
for byte in ba.copy():
    process(byte)

При совместном использовании экземпляров bytearray между потоками рассмотрите возможность внешней синхронизации. Дополнительную информацию см. в разделе Поддержка свободной многопоточности в Python.

Потокобезопасность объектов memoryview

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

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

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

  • Для неизменяемых объектов, таких как bytes, параллельное чтение через несколько объектов memoryview безопасно.
  • Для изменяемых объектов, таких как bytearray, чтение и запись в одну и ту же область памяти из нескольких потоков без внешней синхронизации небезопасны и могут привести к повреждению данных. Обратите внимание: даже доступные только для чтения объекты memoryview изменяемых объектов не предотвращают гонки данных, если базовый объект изменяется из другого потока.
# NOT safe: concurrent writes to the same buffer
data = bytearray(1000)
view = memoryview(data)
# Thread 1: view[0:500] = b'x' * 500
# Thread 2: view[0:500] = b'y' * 500
# Safe: use a lock for concurrent access
import threading
lock = threading.Lock()
data = bytearray(1000)
view = memoryview(data)

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

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

© 2001 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/library/threadsafety.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API