SoFunction
Updated on 2024-10-29

A guide to using asyncio's coroutine and Future objects.

Coroutine's relationship with Future

It looks like they are the same, as both can be used to get results asynchronously using the syntax of the

result = await future
result = await coroutine

In fact, coroutines are generator functions that can both take arguments externally and produce results. The advantage of using coroutines is that we can pause a function and resume execution later. For example, in the case of network operations, it is possible to stop the function until a response arrives. During this time, we can switch to another task to continue execution.

A Future is more like a Promise object in Javascript. It's a placeholder whose value will be calculated in the future. In the example above, when we wait for the network IO function to complete, the function gives us a container that the Promise fills when it finishes. Once filled, we can use the callback function to get the actual result.

The Task object is a subclass of Future, which associates a coroutine with a Future, encapsulating the coroutine into a Future object.

You will generally see two methods of task initiation, the

tasks = (
  asyncio.ensure_future(func1()),
  asyncio.ensure_future(func2())
)
loop.run_until_complete(tasks)

respond in singing

tasks = [
  asyncio.ensure_future(func1()),
  asyncio.ensure_future(func2())
  ]
loop.run_until_complete((tasks))

ensure_future wraps coroutines into Tasks. Wraps some Future and coroutines into a Future.

Then it is itself a coroutine.

run_until_complete can receive both Future and coroutine objects.

BaseEventLoop.run_until_complete(future)

Run until the Future is done.
If the argument is a coroutine object, it is wrapped by ensure_future().
Return the Future's result, or raise its exception.

Task The correct way to exit a task

In asyncio's task loop, if you exit with CTRL-C, even if the exception is caught, the task in the Event Loop will report the following error.

Task was destroyed but it is pending!
task: <Task pending coro=<kill_me() done, defined at :5> wait_for=<Future pending cb=[Task._wakeup()]>>

According to the official documentation, a Task object is considered to exit only in the following cases, the

a result / exception are available, or that the future was cancelled

The cancel of a Task object is slightly different from its parent Future. When () is called, the corresponding coroutine throws a CancelledError exception in the next round of the event loop. The use of () does not return True immediately (which is used to indicate the end of the task); the task is not canceled until the above exception is handled.

Therefore, ending the task can be done with

for task in .all_tasks():
  ()

This method will find all the tasks and cancel them.

But CTRL-C also stops the event loop, so it is necessary to restart the event loop.

try:
  loop.run_until_complete(tasks)
except KeyboardInterrupt as e:
  for task in .all_tasks():
    ()
  loop.run_forever() # restart loop
finally:
  ()

It is necessary to catch exceptions in each Task, and if you are not sure, you can use the

(..., return_exceptions=True)

Returns an exception converted to a normal result.