@@ -248,3 +248,225 @@ Output in debug mode::
248248 File "../t.py", line 4, in bug
249249 raise Exception("not consumed")
250250 Exception: not consumed
251+
252+
253+ Asynchronous generators best practices
254+ ======================================
255+
256+ Writing correct and efficient asyncio code requires awareness of certain pitfalls.
257+ This section outlines essential best practices that can save you hours of debugging.
258+
259+
260+ Close asynchronous generators explicitly
261+ ----------------------------------------
262+
263+ It is recommended to manually close the
264+ :term: `asynchronous generator <asynchronous generator iterator> `. If a generator
265+ exits early - for example, due to an exception raised in the body of
266+ an ``async for `` loop - its asynchronous cleanup code may run in an
267+ unexpected context. This can occur after the tasks it depends on have completed,
268+ or during the event loop shutdown when the async-generator's garbage collection
269+ hook is called.
270+
271+ To avoid this, explicitly close the generator by calling its
272+ :meth: `~agen.aclose ` method, or use the :func: `contextlib.aclosing `
273+ context manager::
274+
275+ import asyncio
276+ import contextlib
277+
278+ async def gen():
279+ yield 1
280+ yield 2
281+
282+ async def func():
283+ async with contextlib.aclosing(gen()) as g:
284+ async for x in g:
285+ break # Don't iterate until the end
286+
287+ asyncio.run(func())
288+
289+ As noted above, the cleanup code for these asynchronous generators is deferred.
290+ The following example demonstrates that the finalization of an asynchronous
291+ generator can occur in an unexpected order::
292+
293+ import asyncio
294+ work_done = False
295+
296+ async def cursor():
297+ try:
298+ yield 1
299+ finally:
300+ assert work_done
301+
302+ async def rows():
303+ global work_done
304+ try:
305+ yield 2
306+ finally:
307+ await asyncio.sleep(0.1) # immitate some async work
308+ work_done = True
309+
310+
311+ async def main():
312+ async for c in cursor():
313+ async for r in rows():
314+ break
315+ break
316+
317+ asyncio.run(main())
318+
319+ For this example, we get the following output::
320+
321+ unhandled exception during asyncio.run() shutdown
322+ task: <Task finished name='Task-3' coro=<<async_generator_athrow without __name__>()> exception=AssertionError()>
323+ Traceback (most recent call last):
324+ File "example.py", line 6, in cursor
325+ yield 1
326+ asyncio.exceptions.CancelledError
327+
328+ During handling of the above exception, another exception occurred:
329+
330+ Traceback (most recent call last):
331+ File "example.py", line 8, in cursor
332+ assert work_done
333+ ^^^^^^^^^
334+ AssertionError
335+
336+ The ``cursor() `` asynchronous generator was finalized before the ``rows ``
337+ generator - an unexpected behavior.
338+
339+ The example can be fixed by explicitly closing the
340+ ``cursor `` and ``rows `` async-generators::
341+
342+ async def main():
343+ async with contextlib.aclosing(cursor()) as cursor_gen:
344+ async for c in cursor_gen:
345+ async with contextlib.aclosing(rows()) as rows_gen:
346+ async for r in rows_gen:
347+ break
348+ break
349+
350+
351+ Create asynchronous generators only when the event loop is running
352+ ------------------------------------------------------------------
353+
354+ It is recommended to create
355+ :term: `asynchronous generators <asynchronous generator iterator> ` only after
356+ the event loop has been created.
357+
358+ To ensure that asynchronous generators close reliably, the event loop uses the
359+ :func: `sys.set_asyncgen_hooks ` function to register callback functions. These
360+ callbacks update the list of running asynchronous generators to keep it in a
361+ consistent state.
362+
363+ When the :meth: `loop.shutdown_asyncgens() <asyncio.loop.shutdown_asyncgens> `
364+ function is called, the running generators are stopped gracefully and the
365+ list is cleared.
366+
367+ The asynchronous generator invokes the corresponding system hook during its
368+ first iteration. At the same time, the generator records that the hook has
369+ been called and does not call it again.
370+
371+ Therefore, if iteration begins before the event loop is created,
372+ the event loop will not be able to add the generator to its list of active
373+ generators because the hooks are set after the generator attempts to call them.
374+ Consequently, the event loop will not be able to terminate the generator
375+ if necessary.
376+
377+ Consider the following example::
378+
379+ import asyncio
380+
381+ async def agenfn():
382+ try:
383+ yield 10
384+ finally:
385+ await asyncio.sleep(0)
386+
387+
388+ with asyncio.Runner() as runner:
389+ agen = agenfn()
390+ print(runner.run(anext(agen)))
391+ del agen
392+
393+ Output::
394+
395+ 10
396+ Exception ignored while closing generator <async_generator object agenfn at 0x000002F71CD10D70>:
397+ Traceback (most recent call last):
398+ File "example.py", line 13, in <module>
399+ del agen
400+ ^^^^
401+ RuntimeError: async generator ignored GeneratorExit
402+
403+ This example can be fixed as follows::
404+
405+ import asyncio
406+
407+ async def agenfn():
408+ try:
409+ yield 10
410+ finally:
411+ await asyncio.sleep(0)
412+
413+ async def main():
414+ agen = agenfn()
415+ print(await anext(agen))
416+ del agen
417+
418+ asyncio.run(main())
419+
420+
421+ Avoid concurrent iteration and closure of the same generator
422+ ------------------------------------------------------------
423+
424+ Async generators may be reentered while another
425+ :meth: `~agen.__anext__ ` / :meth: `~agen.athrow ` / :meth: `~agen.aclose ` call is in
426+ progress. This may lead to an inconsistent state of the async generator and can
427+ cause errors.
428+
429+ Let's consider the following example::
430+
431+ import asyncio
432+
433+ async def consumer():
434+ for idx in range(100):
435+ await asyncio.sleep(0)
436+ message = yield idx
437+ print('received', message)
438+
439+ async def amain():
440+ agenerator = consumer()
441+ await agenerator.asend(None)
442+
443+ fa = asyncio.create_task(agenerator.asend('A'))
444+ fb = asyncio.create_task(agenerator.asend('B'))
445+ await fa
446+ await fb
447+
448+ asyncio.run(amain())
449+
450+ Output::
451+
452+ received A
453+ Traceback (most recent call last):
454+ File "test.py", line 38, in <module>
455+ asyncio.run(amain())
456+ ~~~~~~~~~~~^^^^^^^^^
457+ File "Lib/asyncio/runners.py", line 204, in run
458+ return runner.run(main)
459+ ~~~~~~~~~~^^^^^^
460+ File "Lib/asyncio/runners.py", line 127, in run
461+ return self._loop.run_until_complete(task)
462+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
463+ File "Lib/asyncio/base_events.py", line 719, in run_until_complete
464+ return future.result()
465+ ~~~~~~~~~~~~~^^
466+ File "test.py", line 36, in amain
467+ await fb
468+ RuntimeError: anext(): asynchronous generator is already running
469+
470+
471+ Therefore, it is recommended to avoid using asynchronous generators in parallel
472+ tasks or across multiple event loops.
0 commit comments