blob: 5b5949d4eff437b56167d79eee8b15ecbf49e4ad [file] [log] [blame] [edit]
.. _threadsafety:
************************
Thread Safety Guarantees
************************
This page documents thread-safety guarantees for built-in types in Python's
free-threaded build. The guarantees described here apply when using Python with
the :term:`GIL` disabled (free-threaded mode). When the GIL is enabled, most
operations are implicitly serialized.
For general guidance on writing thread-safe code in free-threaded Python, see
:ref:`freethreading-python-howto`.
.. _thread-safety-list:
Thread safety for list objects
==============================
Reading a single element from a :class:`list` is
:term:`atomic <atomic operation>`:
.. code-block::
:class: good
lst[i] # list.__getitem__
The following methods traverse the list and use :term:`atomic <atomic operation>`
reads of each item to perform their function. That means that they may
return results affected by concurrent modifications:
.. code-block::
:class: maybe
item in lst
lst.index(item)
lst.count(item)
All of the above operations avoid acquiring :term:`per-object locks
<per-object lock>`. They do not block concurrent modifications. Other
operations that hold a lock will not block these from observing intermediate
states.
All other operations from here on block using the :term:`per-object lock`.
Writing a single item via ``lst[i] = x`` is safe to call from multiple
threads and will not corrupt the list.
The following operations return new objects and appear
:term:`atomic <atomic operation>` to other threads:
.. code-block::
:class: good
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
The following methods that only operate on a single element with no shifting
required are :term:`atomic <atomic operation>`:
.. code-block::
:class: good
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
The :meth:`~list.clear` method is also :term:`atomic <atomic operation>`.
Other threads cannot observe elements being removed.
The :meth:`~list.sort` method is not :term:`atomic <atomic operation>`.
Other threads cannot observe intermediate states during sorting, but the
list appears empty for the duration of the sort.
The following operations may allow :term:`lock-free` operations to observe
intermediate states since they modify multiple elements in place:
.. code-block::
:class: maybe
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
The :meth:`~list.remove` method may allow concurrent modifications since
element comparison may execute arbitrary Python code (via
:meth:`~object.__eq__`).
:meth:`~list.extend` is safe to call from multiple threads. However, its
guarantees depend on the iterable passed to it. If it is a :class:`list`, a
:class:`tuple`, a :class:`set`, a :class:`frozenset`, a :class:`dict` or a
:ref:`dictionary view object <dict-views>` (but not their subclasses), the
``extend`` operation is safe from concurrent modifications to the iterable.
Otherwise, an iterator is created which can be concurrently modified by
another thread. The same applies to inplace concatenation of a list with
other iterables when using ``lst += iterable``.
Similarly, assigning to a list slice with ``lst[i:j] = iterable`` is safe
to call from multiple threads, but ``iterable`` is only locked when it is
also a :class:`list` (but not its subclasses).
Operations that involve multiple accesses, as well as iteration, are never
atomic. For example:
.. code-block::
:class: bad
# 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
Consider external synchronization when sharing :class:`list` instances
across threads.
.. _thread-safety-dict:
Thread safety for dict objects
==============================
Creating a dictionary with the :class:`dict` constructor is atomic when the
argument to it is a :class:`dict` or a :class:`tuple`. When using the
:meth:`dict.fromkeys` method, dictionary creation is atomic when the
argument is a :class:`dict`, :class:`tuple`, :class:`set` or
:class:`frozenset`.
The following operations and functions are :term:`lock-free` and
:term:`atomic <atomic operation>`.
.. code-block::
:class: good
d[key] # dict.__getitem__
d.get(key) # dict.get
key in d # dict.__contains__
len(d) # dict.__len__
All other operations from here on hold the :term:`per-object lock`.
Writing or removing a single item is safe to call from multiple threads
and will not corrupt the dictionary:
.. code-block::
:class: good
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
These operations may compare keys using :meth:`~object.__eq__`, which can
execute arbitrary Python code. During such comparisons, the dictionary may
be modified by another thread. For built-in types like :class:`str`,
:class:`int`, and :class:`float`, that implement :meth:`~object.__eq__` in C,
the underlying lock is not released during comparisons and this is not a
concern.
The following operations return new objects and hold the :term:`per-object lock`
for the duration of the operation:
.. code-block::
:class: good
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
The :meth:`~dict.clear` method holds the lock for its duration. Other
threads cannot observe elements being removed.
The following operations lock both dictionaries. For :meth:`~dict.update`
and ``|=``, this applies only when the other operand is a :class:`dict`
that uses the standard dict iterator (but not subclasses that override
iteration). For equality comparison, this applies to :class:`dict` and
its subclasses:
.. code-block::
:class: good
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
All comparison operations also compare values using :meth:`~object.__eq__`,
so for non-built-in types the lock may be released during comparison.
:meth:`~dict.fromkeys` locks both the new dictionary and the iterable
when the iterable is exactly a :class:`dict`, :class:`set`, or
:class:`frozenset` (not subclasses):
.. code-block::
:class: good
dict.fromkeys(a_dict) # locks both
dict.fromkeys(a_set) # locks both
dict.fromkeys(a_frozenset) # locks both
When updating from a non-dict iterable, only the target dictionary is
locked. The iterable may be concurrently modified by another thread:
.. code-block::
:class: maybe
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
Operations that involve multiple accesses, as well as iteration, are never
atomic:
.. code-block::
:class: bad
# 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
To avoid time-of-check to time-of-use (TOCTOU) issues, use atomic
operations or handle exceptions:
.. code-block::
:class: good
# Use pop() with default instead of check-then-delete
d.pop(key, None)
# Or handle the exception
try:
del d[key]
except KeyError:
pass
To safely iterate over a dictionary that may be modified by another
thread, iterate over a copy:
.. code-block::
:class: good
# Make a copy to iterate safely
for key, value in d.copy().items():
process(key)
Consider external synchronization when sharing :class:`dict` instances
across threads.