1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
|
.. _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.
|