Skip to content

Commit a11038f

Browse files
committed
gh-142518: Document thread-safety guarantees of set objects
1 parent 80b2b88 commit a11038f

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

Doc/library/stdtypes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5209,6 +5209,11 @@ Note, the *elem* argument to the :meth:`~object.__contains__`,
52095209
:meth:`~set.discard` methods may be a set. To support searching for an equivalent
52105210
frozenset, a temporary one is created from *elem*.
52115211

5212+
.. seealso::
5213+
5214+
For detailed information on thread-safety guarantees for :class:`set`
5215+
objects, see :ref:`thread-safety-set`.
5216+
52125217

52135218
.. _typesmapping:
52145219

Doc/library/threadsafety.rst

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,113 @@ thread, iterate over a copy:
260260
261261
Consider external synchronization when sharing :class:`dict` instances
262262
across threads.
263+
264+
265+
.. _thread-safety-set:
266+
267+
Thread safety for set objects
268+
==============================
269+
270+
The :func:`len` function is lock-free and :term:`atomic <atomic operation>`.
271+
272+
The following read operation is lock-free. It does not block concurrent
273+
modifications and may observe intermediate states from operations that
274+
hold the per-object lock:
275+
276+
.. code-block::
277+
:class: good
278+
279+
elem in s # set.__contains__
280+
281+
This operation may compare elements using :meth:`~object.__eq__`, which can
282+
execute arbitrary Python code. During such comparisons, the set may be
283+
modified by another thread. For built-in types like :class:`str`,
284+
:class:`int`, and :class:`float`, :meth:`!__eq__` does not release the
285+
underlying lock during comparisons and this is not a concern.
286+
287+
All other operations from here on hold the per-object lock.
288+
289+
Adding or removing a single element is safe to call from multiple threads
290+
and will not corrupt the set:
291+
292+
.. code-block::
293+
:class: good
294+
295+
s.add(elem) # add element
296+
s.remove(elem) # remove element, raise if missing
297+
s.discard(elem) # remove element if present
298+
s.pop() # remove and return arbitrary element
299+
300+
These operations also compare elements, so the same :meth:`~object.__eq__`
301+
considerations as above apply.
302+
303+
The following operations return new objects and hold the per-object lock
304+
for the duration:
305+
306+
.. code-block::
307+
:class: good
308+
309+
s.copy() # returns a shallow copy
310+
311+
The :meth:`~set.clear` method holds the lock for its duration. Other
312+
threads cannot observe elements being removed.
313+
314+
The following operations only accept :class:`set` or :class:`frozenset`
315+
as operands and always lock both objects:
316+
317+
.. code-block::
318+
:class: good
319+
320+
s |= other # other must be set/frozenset
321+
s &= other # other must be set/frozenset
322+
s -= other # other must be set/frozenset
323+
s ^= other # other must be set/frozenset
324+
s & other # other must be set/frozenset
325+
s | other # other must be set/frozenset
326+
s - other # other must be set/frozenset
327+
s ^ other # other must be set/frozenset
328+
329+
:meth:`set.update`, :meth:`set.union`, :meth:`set.intersection` and
330+
:meth:`set.difference` can take multiple iterables as arguments. They all
331+
iterate through all the passed iterables and do the following:
332+
333+
* :meth:`set.update` and :meth:`set.union` lock both objects only when
334+
the other operand is a :class:`set`, :class:`frozenset`, or :class:`dict`.
335+
* :meth:`set.intersection` and :meth:`set.difference` always try to lock
336+
all objects.
337+
338+
:meth:`set.symmetric_difference` tries to lock both objects.
339+
340+
The update variants of the above methods also have some differences between
341+
them:
342+
343+
* :meth:`set.difference_update` and :meth:`set.intersection_update` try
344+
to lock all objects.
345+
* :meth:`set.symmetric_difference_update` only lock the argument if it is
346+
of type :class:`set`, :class:`frozenset`, or :class:`dict`.
347+
348+
The following methods always try to lock both objects:
349+
350+
.. code-block::
351+
:class: good
352+
353+
s.isdisjoint(other) # both locked
354+
s.issubset(other) # both locked
355+
s.issuperset(other) # both locked
356+
357+
Operations that involve multiple accesses, as well as iteration, are never
358+
atomic:
359+
360+
.. code-block::
361+
:class: bad
362+
363+
# NOT atomic: check-then-act
364+
if elem in s:
365+
s.remove(elem)
366+
367+
# NOT thread-safe: iteration while modifying
368+
for elem in s:
369+
process(elem) # another thread may modify s
370+
371+
Consider external synchronization when sharing :class:`set` instances
372+
across threads. See :ref:`freethreading-python-howto` for more information.

0 commit comments

Comments
 (0)