How Does Asyncio Actually Work

How does the asyncio module work, why is my updated sample running synchronously?

Asyncio uses an event loop, which selects what task (an independent call chain of coroutines) in the queue to activate next. The event loop can make intelligent decisions as to what task is ready to do actual work. This is why the event loop also is responsible for creating connections and watching file descriptors and other I/O primitives; it gives the event loop insight into when there are I/O operations in progress or when results are available to process.

Whenever you use await, there is an opportunity to return control to the loop which can then pass control to another task. Which task then is picked for execution depends on the exact implementation; the asyncio reference implementation offers multiple choices, but there are other implementations, such as the very, very efficient uvloop implementation.

Your sample is still asynchronous. It just so happens that by replacing the await.sleep() with a synchronous time.sleep() call, inside a new coroutine function, you introduced 2 coroutines into the task callchain that don't yield, and thus influenced in what order they are executed. That they are executed in what appears to be synchronous order is a coincidence. If you switched event loops, or introduced more coroutines (especially some that use I/O), the order can easily be different again.

Moreover, your new coroutines use time.sleep(); this makes your coroutines uncooperative. The event loop is not notified that your code is waiting (time.sleep() will not yield!), so no other coroutine can be executed while time.sleep() is running. time.sleep() simply doesn't return or lets any other code run until the requested amount of time has passed. Contrast this with the asyncio.sleep() implementation, which simply yields to the event loop with a call_later() hook; the event loop now knows that that task won't need any attention until a later time.

Also see asyncio: why isn't it non-blocking by default for a more in-depth discussion of how tasks and the event loop interact. And if you must run blocking, synchronous code that can't be made to cooperate, then use an executor pool to have the blocking code executed in a separate tread or child process to free up the event loop for other, better behaved tasks.

What does asyncio.create_task() do?

What does asyncio.create_task() do?

It submits the coroutine to run "in the background", i.e. concurrently with the current task and all other tasks, switching between them at await points. It returns an awaitable handle called a "task" which you can also use to cancel the execution of the coroutine.

It's one of the central primitives of asyncio, the asyncio equivalent of starting a thread. (In the same analogy, awaiting the task with await is the equivalent of joining a thread.)

Shouldn't the slow_task not be able to run until the completion of the fast_coro

No, because you explicitly used create_task to start slow_task in the background. Had you written something like:

    slow_coro = counter_loop("Slow", 4)
fast_coro = counter_loop("Fast", 2)
fast_val = await fast_coro

...indeed slow_coro would not run because no one would have yet submitted it to the event loop. But create_task does exactly that: submit it to the event loop for execution concurrently with other tasks, the point of switching being any await.

because it was never used in an asyncio.gather method?

asyncio.gather is not the only way to achieve concurrency in asyncio. It's just a utility function that makes it easier to wait for a number of coroutines to all complete, and submit them to the event loop at the same time. create_task does just the submitting, it should have probably been called start_coroutine or something like that.

Why do we have to await slow_task?

We don't have to, it just serves to wait for both coroutines to finish cleanly. The code could have also awaited asyncio.sleep() or something like that. Returning from main() (and the event loop) immediately with some tasks still pending would have worked as well, but it would have printed a warning message indicating a possible bug. Awaiting (or canceling) the task before stopping the event loop is just cleaner.

What really is a task?

It's an asyncio construct that tracks execution of a coroutine in a concrete event loop. When you call create_task, you submit a coroutine for execution and receive back a handle. You can await this handle when you actually need the result, or you can never await it, if you don't care about the result. This handle is the task, and it inherits from Future, which makes it awaitable and also provides the lower-level callback-based interface, such as add_done_callback.

How does asyncio (python) work?

I think (and to be fair, I'm not an expert on this side of python) that the answer is that coroutines by themselves don't really buy you magical parallelism. A coroutine is a way to say "work on this task until ... and then pause." After it pauses, flow control can go work on something else until you decide to start it up again.

Where you can get multiple things working at the same time is if your coroutine ultimately starts something that can be done asyncronously -- e.g. if you can spin up a thread in the coroutine and the thread is working on something that just so happens to release the GIL (i.e. IO). Then you can start the thread in your co-routine and then pause that execution delegating to other tasks. When you're done with those other tasks, hopefully your IO will be complete and you can go on your merry way without having to wait for the IO at all. I believe that this is what asyncio is doing for you.

Note, if you're looking for a good read on co-routines, it's worth checking out this presentation (concurrency starts at about slide #75).

What is the point of having to put await in front of each async function in Python?

await makes the call locally blocking, but the "wait" is transmitted through the async function (which is itself awaited), such that when it reaches the reactor the entire task can be moved to a waitlist and an other can be run instead.

Furthermore you do not need an await, you could also spawn the coroutine (to a separate task, which you may or may not wait on), or use one of the "future combinators" (asyncio.gather, asyncio.wait, ...) to run it concurrently with others.

asyncio - Code is executing synchronously

Using await, by definition, waits for the task main to finish. So your code as-is is no different from the synchronous code you posted above. If you want to run them at the same time (asynchronously), while waiting for the results, you should use asyncio.gather or asyncio.wait instead.

async def async_io():
tasks = []
for i in range(10):
tasks += [main(i)]
await asyncio.gather(*tasks)

If you don't care to wait for all of the main() calls to finish, you can also just use asyncio.create_task(main(i)), which creates a Task object and schedule its execution in the background. In this case, def async_io() doesn't need to be async anymore.

How does asyncio's event loop know when an awaitable resource is ready?

How does the event loop know when a resource is ready or how many time will take to gather data from some source?

The default event loop (based on SelectorEventLoop) uses the selector module to keep track of all the resources to monitor and get notified when new data is ready. BaseSelector.select is where the magic happens.



Related Topics



Leave a reply



Submit