Back to Kbengine

Developing with asyncio

kbe/src/lib/python/Doc/library/asyncio-dev.rst

2.5.126.6 KB
Original Source

.. 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 :option:-X dev Python command line option.

  • 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:data: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:

  • asyncio checks for :ref:coroutines that were not awaited <asyncio-coroutine-not-scheduled> and logs them; this mitigates the "forgotten await" pitfall.

  • 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 100ms 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 callback from a different 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 and to execute subprocesses, 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 to execute blocking code in a different OS thread without blocking the OS thread that the event loop runs in.

.. _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 or even in a different process to avoid blocking block 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:data:logging.INFO, which can be easily adjusted::

logging.getLogger("asyncio").setLevel(logging.WARNING)

.. _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