Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
19ddd2e
Issue-124397: Add free-threading support for iterators.
rhettinger Apr 22, 2026
4aa242d
Add blurb
rhettinger Apr 23, 2026
e0c44be
More wordsmithing
rhettinger Apr 23, 2026
adcb718
Clarify use of the lock. Add message to the ValueError.
rhettinger Apr 23, 2026
4c2bad0
Include "threading" in the reference
rhettinger Apr 23, 2026
d9dde84
Support send(), throw(), and close() for generators.
rhettinger Apr 23, 2026
fcb9ee8
Tweak wording. Add doctest.
rhettinger Apr 23, 2026
314ec67
Merge branch 'main' into iterator_synchronization
rhettinger Apr 23, 2026
802c1e8
Adopt GPS suggestion to make the instance variables private.
rhettinger Apr 28, 2026
201bc76
Adopt GPS suggestion to add an example to the docstring
rhettinger Apr 28, 2026
2384f46
Add test for exceptions in next() calls
rhettinger Apr 28, 2026
8e291b4
Adopt suggestion for more iterator specific names
rhettinger Apr 30, 2026
adfad71
Test the code blocks
rhettinger Apr 30, 2026
b6601a1
Adopt Colesbury suggestion for a generator example
rhettinger Apr 30, 2026
7df1ef7
Use new name in class reference
rhettinger Apr 30, 2026
4efe3f5
Add whatsnew entry
rhettinger Apr 30, 2026
fa733cf
Adopt Peter's suggestion to not use a lazy import
rhettinger May 1, 2026
0e207fb
Note version added
rhettinger May 1, 2026
4802517
Harmonize the examples into parallel form for easy comparison
rhettinger May 1, 2026
06ed996
Update Lib/threading.py
rhettinger May 1, 2026
a81276a
Update Lib/threading.py
rhettinger May 1, 2026
0c0471f
Update Doc/library/threading.rst
rhettinger May 1, 2026
2563d26
Merge branch 'main' into iterator_synchronization
rhettinger May 1, 2026
ed5dd0f
Defer the import of functools
rhettinger May 1, 2026
fbae726
Adopt suggestion from Daniele to verify the worker threads actually s…
rhettinger May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1436,3 +1436,155 @@ is equivalent to::
Currently, :class:`Lock`, :class:`RLock`, :class:`Condition`,
:class:`Semaphore`, and :class:`BoundedSemaphore` objects may be used as
:keyword:`with` statement context managers.


Iterator synchronization
------------------------

By default, Python iterators do not support concurrent access. Most iterators make
no guarantees when accessed simultaneously from multiple threads. Generator
iterators, for example, raise :exc:`ValueError` if one of their iterator methods
is called while the generator is already executing. The tools in this section
allow reliable concurrency support to be added to ordinary iterators and
iterator-producing callables.

The :class:`serialize_iterator` wrapper lets multiple threads share a single iterator and
take turns consuming from it. While one thread is running ``__next__()``, the
others block until the iterator becomes available. Each value produced by the
underlying iterator is delivered to exactly one caller.

The :func:`concurrent_tee` function lets multiple threads each receive the full
stream of values from one underlying iterator. It creates independent iterators
that all draw from the same source. Values are buffered until consumed by all
of the derived iterators.

.. class:: serialize_iterator(iterable)

Return an iterator wrapper that serializes concurrent calls to
:meth:`~iterator.__next__` using a lock.

If the wrapped iterator also defines :meth:`~generator.send`,
:meth:`~generator.throw`, or :meth:`~generator.close`, those calls
are serialized as well.

This makes it possible to share a single iterator, including a generator
iterator, between multiple threads. A lock assures that calls are handled
Comment thread
rhettinger marked this conversation as resolved.
Outdated
one at a time. No values are duplicated or skipped by the wrapper itself.
Each item from the underlying iterator is given to exactly one caller.

This wrapper does not copy or buffer values. Threads that call
:func:`next` while another thread is already advancing the iterator will
block until the active call completes.

Example:

.. code-block:: python

import threading
Comment thread
rhettinger marked this conversation as resolved.

def count():
for i in range(5):
yield i

it = threading.serialize_iterator(count())

def worker():
for item in it:
print(threading.current_thread().name, item)

threads = [threading.Thread(target=worker) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

In this example, each number is printed exactly once, but the work is shared
between the two threads.

.. versionadded:: next


.. function:: synchronized_iterator(func)
Comment thread
rhettinger marked this conversation as resolved.

Wrap an iterator-producing callable so that each iterator it returns is
automatically passed through :class:`serialize_iterator`.

This is especially useful as a :term:`decorator` for generator functions,
allowing their generator-iterators to be consumed from multiple threads.

Example:

.. code-block:: python

import threading

@threading.synchronized_iterator
def counter():
i = 0
while True:
yield i
i += 1

it = counter()

def worker():
for _ in range(5):
print(next(it))

threads = [threading.Thread(target=worker) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()

The returned wrapper preserves the metadata of *func*, such as its name and
wrapped function reference.

.. versionadded:: next


.. function:: concurrent_tee(iterable, n=2)

Return *n* independent iterators from a single input *iterable*, with
guaranteed behavior when the derived iterators are consumed concurrently.

This function is similar to :func:`itertools.tee`, but is intended for cases
where the source iterator may feed consumers running in different threads.
Each returned iterator yields every value from the underlying iterable, in
the same order.

Internally, values are buffered until every derived iterator has consumed
them.

The returned iterators share the same underlying synchronization lock. Each
individual derived iterator is intended to be consumed by one thread at a
time. If a single derived iterator must itself be shared by multiple
threads, wrap it with :class:`serialize_iterator`.

If *n* is ``0``, return an empty tuple. If *n* is negative, raise
:exc:`ValueError`.

Example:

.. code-block:: python

import threading

source = (x**2 for x in range(5))
left, right = threading.concurrent_tee(source)

def consume(name, iterable):
for item in iterable:
print(name, item)

t1 = threading.Thread(target=consume, args=("left", left))
t2 = threading.Thread(target=consume, args=("right", right))
t1.start()
t2.start()
t1.join()
t2.join()

In this example, both consumer threads see the full sequence of squares
from a single generator expression.

.. versionadded:: next
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,16 @@ tarfile
(Contributed by Christoph Walcher in :gh:`57911`.)


threading
---------

* Added :class:`~threading.serialize_iterator`,
:func:`~threading.synchronized_iterator`,
and :func:`~threading.concurrent_tee` to support concurrent access to
generators and iterators.
(Contributed by Raymond Hettinger in :gh:`124397`.)


timeit
------

Expand Down
Loading
Loading