Skip to content

Commit 8fb10ee

Browse files
Add async generators best practices section
1 parent 4fe6e81 commit 8fb10ee

File tree

1 file changed

+102
-0
lines changed

1 file changed

+102
-0
lines changed

Doc/library/asyncio-dev.rst

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)