@@ -1436,3 +1436,138 @@ is equivalent to::
14361436Currently, :class: `Lock `, :class: `RLock `, :class: `Condition `,
14371437:class: `Semaphore `, and :class: `BoundedSemaphore ` objects may be used as
14381438:keyword: `with ` statement context managers.
1439+
1440+
1441+ Iterator synchronization
1442+ ------------------------
1443+
1444+ By default, Python iterators do not support concurrent use. Most iterators make
1445+ no guarantees when accessed simultaneously from multiple threads. Generator
1446+ iterators, for example, raise :exc: `ValueError ` if one of their iterator methods
1447+ is called while the generator is already executing. The tools in this section
1448+ allow reliable concurrency support to be added to ordinary iterators and
1449+ iterator-producing callables.
1450+
1451+ Use :class: `serialize ` when multiple threads should share a single iterator and
1452+ take turns consuming from it. While one thread is running ``__next__() ``, the
1453+ others block until the iterator becomes available. Each value produced by the
1454+ underlying iterator is delivered to exactly one caller.
1455+
1456+ Use :func: `concurrent_tee ` when multiple threads should each receive the full
1457+ stream of values from one underlying iterator. It creates independent iterators
1458+ that all draw from the same source. Values are buffered until every derived
1459+ iterator has received them.
1460+
1461+ .. class :: serialize(iterable)
1462+
1463+ Return an iterator wrapper that serializes concurrent calls to
1464+ :meth: `~iterator.__next__ ` using a lock.
1465+
1466+ This makes it possible to share a single iterator, including a generator
1467+ iterator, between multiple threads. Calls are handled one at a time in
1468+ arrival order determined by lock acquisition. No values are duplicated or
1469+ skipped by the wrapper itself; each item produced by the underlying iterator
1470+ is returned to exactly one caller.
1471+
1472+ This wrapper does not copy or buffer values. Threads that call
1473+ :func: `next ` while another thread is already advancing the iterator will
1474+ block until the active call completes.
1475+
1476+ Example::
1477+
1478+ import threading
1479+
1480+ def count():
1481+ for i in range(5):
1482+ yield i
1483+
1484+ it = threading.serialize(count())
1485+
1486+ def worker():
1487+ for item in it:
1488+ print(threading.current_thread().name, item)
1489+
1490+ threads = [threading.Thread(target=worker) for _ in range(2)]
1491+ for thread in threads:
1492+ thread.start()
1493+ for thread in threads:
1494+ thread.join()
1495+
1496+ In this example, each number is printed exactly once, but the work is shared
1497+ between the two threads.
1498+
1499+ .. function :: synchronized(func)
1500+
1501+ Wrap an iterator-producing callable so that each iterator it returns is
1502+ automatically passed through :class: `serialize `.
1503+
1504+ This is especially useful as a decorator for generator functions that may be
1505+ consumed from multiple threads.
1506+
1507+ Example::
1508+
1509+ import threading
1510+
1511+ @threading.synchronized
1512+ def counter():
1513+ i = 0
1514+ while True:
1515+ yield i
1516+ i += 1
1517+
1518+ it = counter()
1519+
1520+ def worker():
1521+ for _ in range(3):
1522+ print(next(it))
1523+
1524+ threads = [threading.Thread(target=worker) for _ in range(2)]
1525+ for thread in threads:
1526+ thread.start()
1527+ for thread in threads:
1528+ thread.join()
1529+
1530+ The returned wrapper preserves the metadata of *func *, such as its name and
1531+ wrapped function reference.
1532+
1533+ .. function :: concurrent_tee(iterable, n=2)
1534+
1535+ Return *n * independent iterators from a single input *iterable *, with
1536+ guaranteed behavior when the derived iterators are consumed concurrently.
1537+
1538+ This function is similar to :func: `itertools.tee `, but is intended for cases
1539+ where the source iterator may feed consumers running in different threads.
1540+ Each returned iterator yields every value from the underlying iterable, in
1541+ the same order.
1542+
1543+ Internally, values are buffered until every derived iterator has consumed
1544+ them. As a result, if one consumer falls far behind the others, the buffer
1545+ may grow without bound.
1546+
1547+ The returned iterators share the same underlying synchronization lock. Each
1548+ individual derived iterator is intended to be consumed by one thread at a
1549+ time. If a single derived iterator must itself be shared by multiple
1550+ threads, wrap it with :class: `serialize `.
1551+
1552+ If *n * is ``0 ``, return an empty tuple. If *n * is negative, raise
1553+ :exc: `ValueError `.
1554+
1555+ Example::
1556+
1557+ import threading
1558+
1559+ source = range(5)
1560+ left, right = threading.concurrent_tee(source)
1561+
1562+ def consume(name, iterable):
1563+ for item in iterable:
1564+ print(name, item)
1565+
1566+ t1 = threading.Thread(target=consume, args=("left", left))
1567+ t2 = threading.Thread(target=consume, args=("right", right))
1568+ t1.start()
1569+ t2.start()
1570+ t1.join()
1571+ t2.join()
1572+
1573+ Here, both threads see the full sequence ``0 `` through ``4 ``.
0 commit comments