mirror of
https://github.com/python/cpython.git
synced 2026-04-15 16:21:24 +00:00
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> Co-authored-by: Guido van Rossum <gvanrossum@gmail.com> Co-authored-by: Kumar Aditya <kumaraditya@python.org>
472 lines
14 KiB
ReStructuredText
472 lines
14 KiB
ReStructuredText
.. currentmodule:: asyncio
|
|
|
|
.. _asyncio-dev:
|
|
|
|
=======================
|
|
Developing with asyncio
|
|
=======================
|
|
|
|
Asynchronous programming is different from classic "sequential"
|
|
programming.
|
|
|
|
This page lists common mistakes and traps and explains how
|
|
to avoid them.
|
|
|
|
|
|
.. _asyncio-debug-mode:
|
|
|
|
Debug Mode
|
|
==========
|
|
|
|
By default asyncio runs in production mode. In order to ease
|
|
the development asyncio has a *debug mode*.
|
|
|
|
There are several ways to enable asyncio debug mode:
|
|
|
|
* Setting the :envvar:`PYTHONASYNCIODEBUG` environment variable to ``1``.
|
|
|
|
* Using the :ref:`Python Development Mode <devmode>`.
|
|
|
|
* Passing ``debug=True`` to :func:`asyncio.run`.
|
|
|
|
* Calling :meth:`loop.set_debug`.
|
|
|
|
In addition to enabling the debug mode, consider also:
|
|
|
|
* setting the log level of the :ref:`asyncio logger <asyncio-logger>` to
|
|
:py:const:`logging.DEBUG`, for example the following snippet of code
|
|
can be run at startup of the application::
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
* configuring the :mod:`warnings` module to display
|
|
:exc:`ResourceWarning` warnings. One way of doing that is by
|
|
using the :option:`-W` ``default`` command line option.
|
|
|
|
|
|
When the debug mode is enabled:
|
|
|
|
* Many non-threadsafe asyncio APIs (such as :meth:`loop.call_soon` and
|
|
:meth:`loop.call_at` methods) raise an exception if they are called
|
|
from a wrong thread.
|
|
|
|
* The execution time of the I/O selector is logged if it takes too long to
|
|
perform an I/O operation.
|
|
|
|
* Callbacks taking longer than 100 milliseconds are logged. The
|
|
:attr:`loop.slow_callback_duration` attribute can be used to set the
|
|
minimum execution duration in seconds that is considered "slow".
|
|
|
|
|
|
.. _asyncio-multithreading:
|
|
|
|
Concurrency and Multithreading
|
|
==============================
|
|
|
|
An event loop runs in a thread (typically the main thread) and executes
|
|
all callbacks and Tasks in its thread. While a Task is running in the
|
|
event loop, no other Tasks can run in the same thread. When a Task
|
|
executes an ``await`` expression, the running Task gets suspended, and
|
|
the event loop executes the next Task.
|
|
|
|
To schedule a :term:`callback` from another OS thread, the
|
|
:meth:`loop.call_soon_threadsafe` method should be used. Example::
|
|
|
|
loop.call_soon_threadsafe(callback, *args)
|
|
|
|
Almost all asyncio objects are not thread safe, which is typically
|
|
not a problem unless there is code that works with them from outside
|
|
of a Task or a callback. If there's a need for such code to call a
|
|
low-level asyncio API, the :meth:`loop.call_soon_threadsafe` method
|
|
should be used, e.g.::
|
|
|
|
loop.call_soon_threadsafe(fut.cancel)
|
|
|
|
To schedule a coroutine object from a different OS thread, the
|
|
:func:`run_coroutine_threadsafe` function should be used. It returns a
|
|
:class:`concurrent.futures.Future` to access the result::
|
|
|
|
async def coro_func():
|
|
return await asyncio.sleep(1, 42)
|
|
|
|
# Later in another OS thread:
|
|
|
|
future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
|
|
# Wait for the result:
|
|
result = future.result()
|
|
|
|
To handle signals the event loop must be
|
|
run in the main thread.
|
|
|
|
The :meth:`loop.run_in_executor` method can be used with a
|
|
:class:`concurrent.futures.ThreadPoolExecutor` or
|
|
:class:`~concurrent.futures.InterpreterPoolExecutor` to execute
|
|
blocking code in a different OS thread without blocking the OS thread
|
|
that the event loop runs in.
|
|
|
|
There is currently no way to schedule coroutines or callbacks directly
|
|
from a different process (such as one started with
|
|
:mod:`multiprocessing`). The :ref:`asyncio-event-loop-methods`
|
|
section lists APIs that can read from pipes and watch file descriptors
|
|
without blocking the event loop. In addition, asyncio's
|
|
:ref:`Subprocess <asyncio-subprocess>` APIs provide a way to start a
|
|
process and communicate with it from the event loop. Lastly, the
|
|
aforementioned :meth:`loop.run_in_executor` method can also be used
|
|
with a :class:`concurrent.futures.ProcessPoolExecutor` to execute
|
|
code in a different process.
|
|
|
|
.. _asyncio-handle-blocking:
|
|
|
|
Running Blocking Code
|
|
=====================
|
|
|
|
Blocking (CPU-bound) code should not be called directly. For example,
|
|
if a function performs a CPU-intensive calculation for 1 second,
|
|
all concurrent asyncio Tasks and IO operations would be delayed
|
|
by 1 second.
|
|
|
|
An executor can be used to run a task in a different thread,
|
|
including in a different interpreter, or even in
|
|
a different process to avoid blocking the OS thread with the
|
|
event loop. See the :meth:`loop.run_in_executor` method for more
|
|
details.
|
|
|
|
|
|
.. _asyncio-logger:
|
|
|
|
Logging
|
|
=======
|
|
|
|
asyncio uses the :mod:`logging` module and all logging is performed
|
|
via the ``"asyncio"`` logger.
|
|
|
|
The default log level is :py:const:`logging.INFO`, which can be easily
|
|
adjusted::
|
|
|
|
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
|
|
|
|
Network logging can block the event loop. It is recommended to use
|
|
a separate thread for handling logs or use non-blocking IO. For example,
|
|
see :ref:`blocking-handlers`.
|
|
|
|
|
|
.. _asyncio-coroutine-not-scheduled:
|
|
|
|
Detect never-awaited coroutines
|
|
===============================
|
|
|
|
When a coroutine function is called, but not awaited
|
|
(e.g. ``coro()`` instead of ``await coro()``)
|
|
or the coroutine is not scheduled with :meth:`asyncio.create_task`, asyncio
|
|
will emit a :exc:`RuntimeWarning`::
|
|
|
|
import asyncio
|
|
|
|
async def test():
|
|
print("never scheduled")
|
|
|
|
async def main():
|
|
test()
|
|
|
|
asyncio.run(main())
|
|
|
|
Output::
|
|
|
|
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
|
|
test()
|
|
|
|
Output in debug mode::
|
|
|
|
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
|
|
Coroutine created at (most recent call last)
|
|
File "../t.py", line 9, in <module>
|
|
asyncio.run(main(), debug=True)
|
|
|
|
< .. >
|
|
|
|
File "../t.py", line 7, in main
|
|
test()
|
|
test()
|
|
|
|
The usual fix is to either await the coroutine or call the
|
|
:meth:`asyncio.create_task` function::
|
|
|
|
async def main():
|
|
await test()
|
|
|
|
|
|
Detect never-retrieved exceptions
|
|
=================================
|
|
|
|
If a :meth:`Future.set_exception` is called but the Future object is
|
|
never awaited on, the exception would never be propagated to the
|
|
user code. In this case, asyncio would emit a log message when the
|
|
Future object is garbage collected.
|
|
|
|
Example of an unhandled exception::
|
|
|
|
import asyncio
|
|
|
|
async def bug():
|
|
raise Exception("not consumed")
|
|
|
|
async def main():
|
|
asyncio.create_task(bug())
|
|
|
|
asyncio.run(main())
|
|
|
|
Output::
|
|
|
|
Task exception was never retrieved
|
|
future: <Task finished coro=<bug() done, defined at test.py:3>
|
|
exception=Exception('not consumed')>
|
|
|
|
Traceback (most recent call last):
|
|
File "test.py", line 4, in bug
|
|
raise Exception("not consumed")
|
|
Exception: not consumed
|
|
|
|
:ref:`Enable the debug mode <asyncio-debug-mode>` to get the
|
|
traceback where the task was created::
|
|
|
|
asyncio.run(main(), debug=True)
|
|
|
|
Output in debug mode::
|
|
|
|
Task exception was never retrieved
|
|
future: <Task finished coro=<bug() done, defined at test.py:3>
|
|
exception=Exception('not consumed') created at asyncio/tasks.py:321>
|
|
|
|
source_traceback: Object created at (most recent call last):
|
|
File "../t.py", line 9, in <module>
|
|
asyncio.run(main(), debug=True)
|
|
|
|
< .. >
|
|
|
|
Traceback (most recent call last):
|
|
File "../t.py", line 4, in bug
|
|
raise Exception("not consumed")
|
|
Exception: not consumed
|
|
|
|
|
|
Asynchronous generators best practices
|
|
======================================
|
|
|
|
Writing correct and efficient asyncio code requires awareness of certain pitfalls.
|
|
This section outlines essential best practices that can save you hours of debugging.
|
|
|
|
|
|
Close asynchronous generators explicitly
|
|
----------------------------------------
|
|
|
|
It is recommended to manually close the
|
|
:term:`asynchronous generator <asynchronous generator iterator>`. If a generator
|
|
exits early - for example, due to an exception raised in the body of
|
|
an ``async for`` loop - its asynchronous cleanup code may run in an
|
|
unexpected context. This can occur after the tasks it depends on have completed,
|
|
or during the event loop shutdown when the async-generator's garbage collection
|
|
hook is called.
|
|
|
|
To avoid this, explicitly close the generator by calling its
|
|
:meth:`~agen.aclose` method, or use the :func:`contextlib.aclosing`
|
|
context manager::
|
|
|
|
import asyncio
|
|
import contextlib
|
|
|
|
async def gen():
|
|
yield 1
|
|
yield 2
|
|
|
|
async def func():
|
|
async with contextlib.aclosing(gen()) as g:
|
|
async for x in g:
|
|
break # Don't iterate until the end
|
|
|
|
asyncio.run(func())
|
|
|
|
As noted above, the cleanup code for these asynchronous generators is deferred.
|
|
The following example demonstrates that the finalization of an asynchronous
|
|
generator can occur in an unexpected order::
|
|
|
|
import asyncio
|
|
work_done = False
|
|
|
|
async def cursor():
|
|
try:
|
|
yield 1
|
|
finally:
|
|
assert work_done
|
|
|
|
async def rows():
|
|
global work_done
|
|
try:
|
|
yield 2
|
|
finally:
|
|
await asyncio.sleep(0.1) # immitate some async work
|
|
work_done = True
|
|
|
|
|
|
async def main():
|
|
async for c in cursor():
|
|
async for r in rows():
|
|
break
|
|
break
|
|
|
|
asyncio.run(main())
|
|
|
|
For this example, we get the following output::
|
|
|
|
unhandled exception during asyncio.run() shutdown
|
|
task: <Task finished name='Task-3' coro=<<async_generator_athrow without __name__>()> exception=AssertionError()>
|
|
Traceback (most recent call last):
|
|
File "example.py", line 6, in cursor
|
|
yield 1
|
|
asyncio.exceptions.CancelledError
|
|
|
|
During handling of the above exception, another exception occurred:
|
|
|
|
Traceback (most recent call last):
|
|
File "example.py", line 8, in cursor
|
|
assert work_done
|
|
^^^^^^^^^
|
|
AssertionError
|
|
|
|
The ``cursor()`` asynchronous generator was finalized before the ``rows``
|
|
generator - an unexpected behavior.
|
|
|
|
The example can be fixed by explicitly closing the
|
|
``cursor`` and ``rows`` async-generators::
|
|
|
|
async def main():
|
|
async with contextlib.aclosing(cursor()) as cursor_gen:
|
|
async for c in cursor_gen:
|
|
async with contextlib.aclosing(rows()) as rows_gen:
|
|
async for r in rows_gen:
|
|
break
|
|
break
|
|
|
|
|
|
Create asynchronous generators only when the event loop is running
|
|
------------------------------------------------------------------
|
|
|
|
It is recommended to create
|
|
:term:`asynchronous generators <asynchronous generator iterator>` only after
|
|
the event loop has been created.
|
|
|
|
To ensure that asynchronous generators close reliably, the event loop uses the
|
|
:func:`sys.set_asyncgen_hooks` function to register callback functions. These
|
|
callbacks update the list of running asynchronous generators to keep it in a
|
|
consistent state.
|
|
|
|
When the :meth:`loop.shutdown_asyncgens() <asyncio.loop.shutdown_asyncgens>`
|
|
function is called, the running generators are stopped gracefully and the
|
|
list is cleared.
|
|
|
|
The asynchronous generator invokes the corresponding system hook during its
|
|
first iteration. At the same time, the generator records that the hook has
|
|
been called and does not call it again.
|
|
|
|
Therefore, if iteration begins before the event loop is created,
|
|
the event loop will not be able to add the generator to its list of active
|
|
generators because the hooks are set after the generator attempts to call them.
|
|
Consequently, the event loop will not be able to terminate the generator
|
|
if necessary.
|
|
|
|
Consider the following example::
|
|
|
|
import asyncio
|
|
|
|
async def agenfn():
|
|
try:
|
|
yield 10
|
|
finally:
|
|
await asyncio.sleep(0)
|
|
|
|
|
|
with asyncio.Runner() as runner:
|
|
agen = agenfn()
|
|
print(runner.run(anext(agen)))
|
|
del agen
|
|
|
|
Output::
|
|
|
|
10
|
|
Exception ignored while closing generator <async_generator object agenfn at 0x000002F71CD10D70>:
|
|
Traceback (most recent call last):
|
|
File "example.py", line 13, in <module>
|
|
del agen
|
|
^^^^
|
|
RuntimeError: async generator ignored GeneratorExit
|
|
|
|
This example can be fixed as follows::
|
|
|
|
import asyncio
|
|
|
|
async def agenfn():
|
|
try:
|
|
yield 10
|
|
finally:
|
|
await asyncio.sleep(0)
|
|
|
|
async def main():
|
|
agen = agenfn()
|
|
print(await anext(agen))
|
|
del agen
|
|
|
|
asyncio.run(main())
|
|
|
|
|
|
Avoid concurrent iteration and closure of the same generator
|
|
------------------------------------------------------------
|
|
|
|
Async generators may be reentered while another
|
|
:meth:`~agen.__anext__` / :meth:`~agen.athrow` / :meth:`~agen.aclose` call is in
|
|
progress. This may lead to an inconsistent state of the async generator and can
|
|
cause errors.
|
|
|
|
Let's consider the following example::
|
|
|
|
import asyncio
|
|
|
|
async def consumer():
|
|
for idx in range(100):
|
|
await asyncio.sleep(0)
|
|
message = yield idx
|
|
print('received', message)
|
|
|
|
async def amain():
|
|
agenerator = consumer()
|
|
await agenerator.asend(None)
|
|
|
|
fa = asyncio.create_task(agenerator.asend('A'))
|
|
fb = asyncio.create_task(agenerator.asend('B'))
|
|
await fa
|
|
await fb
|
|
|
|
asyncio.run(amain())
|
|
|
|
Output::
|
|
|
|
received A
|
|
Traceback (most recent call last):
|
|
File "test.py", line 38, in <module>
|
|
asyncio.run(amain())
|
|
~~~~~~~~~~~^^^^^^^^^
|
|
File "Lib/asyncio/runners.py", line 204, in run
|
|
return runner.run(main)
|
|
~~~~~~~~~~^^^^^^
|
|
File "Lib/asyncio/runners.py", line 127, in run
|
|
return self._loop.run_until_complete(task)
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
|
|
File "Lib/asyncio/base_events.py", line 719, in run_until_complete
|
|
return future.result()
|
|
~~~~~~~~~~~~~^^
|
|
File "test.py", line 36, in amain
|
|
await fb
|
|
RuntimeError: anext(): asynchronous generator is already running
|
|
|
|
|
|
Therefore, it is recommended to avoid using asynchronous generators in parallel
|
|
tasks or across multiple event loops.
|