@@ -248,3 +248,105 @@ Output in debug mode::
248248 File "../t.py", line 4, in bug
249249 raise Exception("not consumed")
250250 Exception: not consumed
251+
252+ Asynchronous generators best practices
253+ ======================================
254+
255+ By :term: `asynchronous generator ` in this section we will mean
256+ an :term: `asynchronous generator iterator ` that returned by
257+ :term: `asynchronous generator ` function.
258+
259+ Manually close the generator
260+ ----------------------------
261+
262+ If an asynchronous generator happens to exit early by :keyword: `break `, the caller
263+ task being cancelled, or other exceptions, the generator's async cleanup code
264+ will run and possibly raise exceptions or access context variables in an
265+ unexpected context--perhaps after the lifetime of tasks it depends, or
266+ during the event loop shutdown when the async-generator garbage collection hook
267+ is called.
268+
269+ To prevent this, it is recommended to explicitly close the async generator by
270+ calling :meth: `~agen.aclose ` method, or using :func: `contextlib.aclosing ` context
271+ manager::
272+
273+ import asyncio
274+ import contextlib
275+
276+ async def gen():
277+ yield 1
278+ yield 2
279+
280+ async def func():
281+ async with contextlib.aclosing(gen()) as g:
282+ async for x in g:
283+ break
284+
285+ asyncio.run(func())
286+
287+
288+ Only create a generator when a loop is already running
289+ ------------------------------------------------------
290+
291+ As said abovew, if an asynchronous generator is not resumed before it is
292+ finalized, then any finalization procedures will be delayed. The event loop
293+ handles this situation and doing it best to call async generator-iterator's
294+ :meth: `~agen.aclose ` at the proper moment, thus allowing any pending
295+ :keyword: `!finally ` clauses to execute.
296+
297+ Then it is recomended to create async generators only after the event loop
298+ has already been created.
299+
300+
301+ Avoid iterating and closing the same generator concurrently
302+ -----------------------------------------------------------
303+
304+ The async generators allow to be reentered while another
305+ :meth: `~agen.aclose `/:meth: `~agen.aclose `/:meth: `~agen.aclose ` call is in
306+ progress. This may lead to an inconsistent state of the async generator
307+ and cause errors.
308+
309+ Let's consider following example::
310+
311+ import asyncio
312+
313+ async def consumer():
314+ for idx in range(100):
315+ await asyncio.sleep(0)
316+ message = yield idx
317+ print('received', message)
318+
319+ async def amain():
320+ agenerator = consumer()
321+ await agenerator.asend(None)
322+
323+ fa = asyncio.create_task(agenerator.asend('A'))
324+ fb = asyncio.create_task(agenerator.asend('B'))
325+ await fa
326+ await fb
327+
328+ asyncio.run(amain())
329+
330+ Output::
331+
332+ received A
333+ Traceback (most recent call last):
334+ File "test.py", line 38, in <module>
335+ asyncio.run(amain())
336+ ~~~~~~~~~~~^^^^^^^^^
337+ File "Lib\asyncio\runners.py", line 204, in run
338+ return runner.run(main)
339+ ~~~~~~~~~~^^^^^^
340+ File "Lib\asyncio\runners.py", line 127, in run
341+ return self._loop.run_until_complete(task)
342+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
343+ File "Lib\asyncio\base_events.py", line 719, in run_until_complete
344+ return future.result()
345+ ~~~~~~~~~~~~~^^
346+ File "test.py", line 36, in amain
347+ await fb
348+ RuntimeError: anext(): asynchronous generator is already running
349+
350+
351+ Therefore, it is recommended to avoid using the async generators in parallel
352+ tasks or in the multiple event loops.
0 commit comments