@@ -1465,6 +1465,182 @@ don't have any special access to Trio's internals.)
1465
1465
:members:
1466
1466
1467
1467
1468
+ .. _async-generators :
1469
+
1470
+ Notes on async generators
1471
+ -------------------------
1472
+
1473
+ Python 3.6 added support for *async generators *, which can use
1474
+ ``await ``, ``async for ``, and ``async with `` in between their ``yield ``
1475
+ statements. As you might expect, you use ``async for `` to iterate
1476
+ over them. :pep: `525 ` has many more details if you want them.
1477
+
1478
+ For example, the following is a roundabout way to print
1479
+ the numbers 0 through 9 with a 1-second delay before each one::
1480
+
1481
+ async def range_slowly(*args):
1482
+ """Like range(), but adds a 1-second sleep before each value."""
1483
+ for value in range(*args):
1484
+ await trio.sleep(1)
1485
+ yield value
1486
+
1487
+ async def use_it():
1488
+ async for value in range_slowly(10):
1489
+ print(value)
1490
+
1491
+ trio.run(use_it)
1492
+
1493
+ Trio supports async generators, with some caveats described in this section.
1494
+
1495
+ Finalization
1496
+ ~~~~~~~~~~~~
1497
+
1498
+ If you iterate over an async generator in its entirety, like the
1499
+ example above does, then the execution of the async generator will
1500
+ occur completely in the context of the code that's iterating over it,
1501
+ and there aren't too many surprises.
1502
+
1503
+ If you abandon a partially-completed async generator, though, such as
1504
+ by ``break ``\i ng out of the iteration, things aren't so simple. The
1505
+ async generator iterator object is still alive, waiting for you to
1506
+ resume iterating it so it can produce more values. At some point,
1507
+ Python will realize that you've dropped all references to the
1508
+ iterator, and will call on Trio to throw in a `GeneratorExit ` exception
1509
+ so that any remaining cleanup code inside the generator has a chance
1510
+ to run: ``finally `` blocks, ``__aexit__ `` handlers, and so on.
1511
+
1512
+ So far, so good. Unfortunately, Python provides no guarantees about
1513
+ *when * this happens. It could be as soon as you break out of the
1514
+ ``async for `` loop, or an arbitrary amount of time later. It could
1515
+ even be after the entire Trio run has finished! Just about the only
1516
+ guarantee is that it *won't * happen in the task that was using the
1517
+ generator. That task will continue on with whatever else it's doing,
1518
+ and the async generator cleanup will happen "sometime later,
1519
+ somewhere else": potentially with different context variables,
1520
+ not subject to timeouts, and/or after any nurseries you're using have
1521
+ been closed.
1522
+
1523
+ If you don't like that ambiguity, and you want to ensure that a
1524
+ generator's ``finally `` blocks and ``__aexit__ `` handlers execute as
1525
+ soon as you're done using it, then you'll need to wrap your use of the
1526
+ generator in something like `async_generator.aclosing()
1527
+ <https://async-generator.readthedocs.io/en/latest/reference.html#context-managers> `__::
1528
+
1529
+ # Instead of this:
1530
+ async for value in my_generator():
1531
+ if value == 42:
1532
+ break
1533
+
1534
+ # Do this:
1535
+ async with aclosing(my_generator()) as aiter:
1536
+ async for value in aiter:
1537
+ if value == 42:
1538
+ break
1539
+
1540
+ This is cumbersome, but Python unfortunately doesn't provide any other
1541
+ reliable options. If you use ``aclosing() ``, then
1542
+ your generator's cleanup code executes in the same context as the
1543
+ rest of its iterations, so timeouts, exceptions, and context
1544
+ variables work like you'd expect.
1545
+
1546
+ If you don't use ``aclosing() ``, then Trio will do
1547
+ its best anyway, but you'll have to contend with the following semantics:
1548
+
1549
+ * The cleanup of the generator occurs in a cancelled context, i.e.,
1550
+ all blocking calls executed during cleanup will raise `Cancelled `.
1551
+ This is to compensate for the fact that any timeouts surrounding
1552
+ the original use of the generator have been long since forgotten.
1553
+
1554
+ * The cleanup runs without access to any :ref: `context variables
1555
+ <task-local-storage>` that may have been present when the generator
1556
+ was originally being used.
1557
+
1558
+ * If the generator raises an exception during cleanup, then it's
1559
+ printed to the ``trio.async_generator_errors `` logger and otherwise
1560
+ ignored.
1561
+
1562
+ * If an async generator is still alive at the end of the whole
1563
+ call to :func: `trio.run `, then it will be cleaned up after all
1564
+ tasks have exited and before :func: `trio.run ` returns.
1565
+ Since the "system nursery" has already been closed at this point,
1566
+ Trio isn't able to support any new calls to
1567
+ :func: `trio.lowlevel.spawn_system_task `.
1568
+
1569
+ If you plan to run your code on PyPy to take advantage of its better
1570
+ performance, you should be aware that PyPy is *far more likely * than
1571
+ CPython to perform async generator cleanup at a time well after the
1572
+ last use of the generator. (This is a consequence of the fact that
1573
+ PyPy does not use reference counting to manage memory.) To help catch
1574
+ issues like this, Trio will issue a `ResourceWarning ` (ignored by
1575
+ default, but enabled when running under ``python -X dev `` for example)
1576
+ for each async generator that needs to be handled through the fallback
1577
+ finalization path.
1578
+
1579
+ Cancel scopes and nurseries
1580
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
1581
+
1582
+ .. warning :: You may not write a ``yield`` statement that suspends an async generator
1583
+ inside a `CancelScope ` or `Nursery ` that was entered within the generator.
1584
+
1585
+ That is, this is OK::
1586
+
1587
+ async def some_agen():
1588
+ with trio.move_on_after(1):
1589
+ await long_operation()
1590
+ yield "first"
1591
+ async with trio.open_nursery() as nursery:
1592
+ nursery.start_soon(task1)
1593
+ nursery.start_soon(task2)
1594
+ yield "second"
1595
+ ...
1596
+
1597
+ But this is not::
1598
+
1599
+ async def some_agen():
1600
+ with trio.move_on_after(1):
1601
+ yield "first"
1602
+ async with trio.open_nursery() as nursery:
1603
+ yield "second"
1604
+ ...
1605
+
1606
+ Async generators decorated with ``@asynccontextmanager `` to serve as
1607
+ the template for an async context manager are *not * subject to this
1608
+ constraint, because ``@asynccontextmanager `` uses them in a limited
1609
+ way that doesn't create problems.
1610
+
1611
+ Violating the rule described in this section will sometimes get you a
1612
+ useful error message, but Trio is not able to detect all such cases,
1613
+ so sometimes you'll get an unhelpful `TrioInternalError `. (And
1614
+ sometimes it will seem to work, which is probably the worst outcome of
1615
+ all, since then you might not notice the issue until you perform some
1616
+ minor refactoring of the generator or the code that's iterating it, or
1617
+ just get unlucky. There is a `proposed Python enhancement
1618
+ <https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091> `__
1619
+ that would at least make it fail consistently.)
1620
+
1621
+ The reason for the restriction on cancel scopes has to do with the
1622
+ difficulty of noticing when a generator gets suspended and
1623
+ resumed. The cancel scopes inside the generator shouldn't affect code
1624
+ running outside the generator, but Trio isn't involved in the process
1625
+ of exiting and reentering the generator, so it would be hard pressed
1626
+ to keep its cancellation plumbing in the correct state. Nurseries
1627
+ use a cancel scope internally, so they have all the problems of cancel
1628
+ scopes plus a number of problems of their own: for example, when
1629
+ the generator is suspended, what should the background tasks do?
1630
+ There's no good way to suspend them, but if they keep running and throw
1631
+ an exception, where can that exception be reraised?
1632
+
1633
+ If you have an async generator that wants to ``yield `` from within a nursery
1634
+ or cancel scope, your best bet is to refactor it to be a separate task
1635
+ that communicates over memory channels.
1636
+
1637
+ For more discussion and some experimental partial workarounds, see
1638
+ Trio issues `264 <https://github.com/python-trio/trio/issues/264 >`__
1639
+ (especially `this comment
1640
+ <https://github.com/python-trio/trio/issues/264#issuecomment-418989328> `__)
1641
+ and `638 <https://github.com/python-trio/trio/issues/638 >`__.
1642
+
1643
+
1468
1644
.. _threads :
1469
1645
1470
1646
Threads (if you must)
0 commit comments