I’m still trying to understand Python’s approach, and it still makes very little sense to me. Hopefully by the time I finish writing this, that will have changed. At least it should help.
(Written Sept. 2020, Python 3.7-3.8, hopefully things won’t be changing much after this. But I tried this once with Python 3.5, and things did change quite a bit after that, so we’ll see.)
Event loops and threads¶
Python calls the thing that schedules the tasks an event loop, which I find confusing. (Just the first of many things I think badly named in this part of Python.) I’d have called the scheduling mechanism a scheduler.
For now, assume that has been set up for you. Given that, you can use methods like
to schedule things without ever having to deal directly with the event loop.
asyncio.create_task() doesn’t just create a task, it also schedules it.
We’ll see shortly other methods that prepare tasks but don’t schedule them.
If needed, you can get a reference to the event loop (scheduler) easily:
import asyncio loop = asyncio.get_running_loop()
Coroutines functions and objects¶
is a function
allowed to use
when called, does not execute; instead, returns a coroutine object
A coroutine object can be scheduled for execution by passing to
or by using
awaiton it, which blocks and returns the return value.
async def a_coroutine(*args, **kwargs): print("This won't run when the coroutine is called") print("but later, when the return value from the call is scheduled somehow.") return 3 coroutine_object = a_coroutine(1, 2, foo="bar") # no output yet result = await coroutine_object # the print statements in the coroutine function have now run
That’s a lousy example though, because all it really does is rearrange the order of execution you might naively expect.
WRITE MORE HERE
Anything you can use with
await is called an
Many APIs accept awaitables.
Where do we get awaitables?
Coroutine objects are awaitables. They come from calling a coroutine function.
Tasks are awaitables. They come from wrapping coroutine objects using some functions.
Futures are awaitables. They’re returned by some low-level APIs.
Waiting for multiple things¶
Doing things asynchronously but in order¶
Suppose I have:
a coroutine to turn on my tv
a coroutine to set the channel on my tv
and I’m in an event handler that should not block, but can add tasks to the event loop.
If we add both coroutines, they could run in the wrong order, or even roughly simultaneously.
So we write another coroutine:
async def do_both(): await turn_on_tv() await set_tv_channel()
and submit that, and we’re good.
That looks like this:
Note as always that we first have to call the coroutine, then we can use the return value, the coroutine object, to schedule it to run.
Note Most asyncio scheduling functions don’t allow passing keyword arguments. To do that, use functools.partial():
# will schedule "print("Hello", flush=True)" loop.call_soon( functools.partial(print, "Hello", flush=True))
Using partial objects is usually more convenient than using lambdas, as asyncio can render partial objects better in debug and error messages.
How I might have designed this interface¶
I think I’d have focused more on the scheduler and do things with it, and not had special magical methods that don’t run when you call them.
E.g., where in Python today you would write:
import asyncio async def run_this_later(*args, **kwargs): # do stuff asyncio.create_task(run_this_later(*args, **kwargs))
await func1(*args, **kwargs) await func2(...)
I might have written:
from dans_coroutines import scheduler def run_this_later(*args, **kwargs): # do stuff scheduler.queue_to_run(run_this_later, *args, **kwargs)
scheduler.run_until_finished(func1, *args, **kwargs) scheduler.run_until_finished(func2, ...)
It is at calls to scheduler that we might pause execution of the current coroutine and let another one run for a while.
Schedule normal code to run later¶
IS THIS NEEDED ANYMORE?
The Python async event loop lets you do that using loop.call_soon:
import asyncio def thing_to_do_later(1, 2): doing this... asyncio.get_running_loop().call_soon(thing_to_do_later, 1, 2)
To call with with kwargs, you have to use partial as mentioned above:
from functools import partial loop.call_soon(partial(thing_to_do_later, 1, 2, foo="Bar", thing=27))
Some things to keep in mind:
A Python thread can have at most one active event loop.
Multiple threads can each have their own event loop.
asyncio.get_running_loop()returns the loop of the current thread.
It is not safe to access a loop belonging to another thread than the current thread, with a few exceptions.
None of this will be a problem until your program becomes multi-threaded, of course, and with all this async stuff, you may never need multiple threads.