87

I was going through the Python documentation for asyncio and I'm wondering why most examples use loop.run_until_complete() as opposed to Asyncio.ensure_future().

For example: https://docs.python.org/dev/library/asyncio-task.html

It seems ensure_future would be a much better way to demonstrate the advantages of non-blocking functions. run_until_complete on the other hand, blocks the loop like synchronous functions do.

This makes me feel like I should be using run_until_complete instead of a combination of ensure_futurewith loop.run_forever() to run multiple co-routines concurrently.

3
  • 6
    run_until_complete doesn't block anything. The difference between it and run_forever is that the loop pauses at the completion of the coroutine. The only time it will block is if your coroutine never awaits. Commented Oct 20, 2016 at 1:01
  • 2
    I wrote this pastebin.com/Qi8dQ3bh and it does seem to block the loop though. do_other_things() doesn't execute until do_io() is done, even though do_io() awaits a the sleep. Commented Oct 20, 2016 at 16:49
  • 5
    That's because nothing else has been scheduled with the loop. Try calling loop.create_task(do_other_things()) before you call run_forever. Commented Oct 20, 2016 at 17:01

3 Answers 3

102

run_until_complete is used to run a future until it's finished. It will block the execution of code following it. It does, however, cause the event loop to run. Any futures that have been scheduled will run until the future passed to run_until_complete is done.

Given this example:

import asyncio

async def do_io():
    print('io start')
    await asyncio.sleep(5)
    print('io end')

async def do_other_things():
    print('doing other things')

loop = asyncio.get_event_loop()

loop.run_until_complete(do_io())
loop.run_until_complete(do_other_things())

loop.close()

do_io will run. After it's complete, do_other_things will run. Your output will be:

io start
io end
doing other things

If you schedule do_other_things with the event loop before running do_io, control will switch from do_io to do_other_things when the former awaits.

loop.create_task(do_other_things())
loop.run_until_complete(do_io())

This will get you the output of:

doing other things
io start
io end

This is because do_other_things was scheduled before do_io. There are a lot of different ways to get the same output, but which one makes sense really depends on what your application actually does. So I'll leave that as an exercise to the reader.

Sign up to request clarification or add additional context in comments.

6 Comments

Do you know why I get the error "RuntimeError: This event loop is already running" when I run your code?
@Patrick did you try calling loop.run_until_complete from inside a function?
I realized that the problem maybe with Jupyter. It works with python code.
get_event_loop is deprecated : docs.python.org/3/library/…
@baxx it's not deprecated, only using this function without running even loop is deprecated.
|
17

I think most people didn't understand create_task. when you create_task or ensure_future, it will be scheduled already.

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello')) # not block here

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}") # time0

    await task1 # block here!

    print(f"finished at {time.strftime('%X')}") 
    
    await task2 # block here!

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

result is

time0 
print hello 
time0+1 
print world 
time0+2

but IF YOU DON'T AWAIT task1, do something else

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello')) # not block here
    print(f"finished at {time.strftime('%X')}") # time0
    await asyncio.sleep(2) # not await task1
    print(f"finished at {time.strftime('%X')}") # time0+2
 

asyncio.run(main())

it will do task1 STILL

time0
print hello
time0+2

3 Comments

Nice, don't forget to import asyncio import time
This answer is good because it uses modern Python 3.10+ asyncio.create_task/asyncio.run. Reducing it to just those two along with asyncio.gather, asyncio.sleep and async/await themselves simplifies things a lot.
Note that asyncio.sleep is almost never needed in real async code, it's just a useful tool to demonstrate async behavior. I think I've used it precisely once in real code, and only because I was mixing asyncio and tkinter code, and implemented a sort of combined event loop where asyncio would tell tkinter to process all ready tasks, then asyncio.sleep for _tkinter.getbusywaitinterval() / 1000 seconds to allow the scheduled ayncio tasks to run without constantly spinning to recheck for tkinter events more than needed.
3

Because once upon a time that's all there was.

You need to consider the evolution of asyncio here. It's like

  • hey we can now use yield to suspend functions to make generators and closures and stuff

  • hey we added yield from so people don't need awkward loops for sub-generators

  • we can put a Future and a coroutine together, call it a task. Also, let's add some syntax like async and await so we don't need those awkward @coroutine and yield from hacks any more.

  • So to get from the sync world to the async one, we set up an event loop and use run_until_complete to get that task's result. Or you can just run_forever if you don't need any particular value, like when you write a server. (The result was asyncio, from 2014.)

  • Umm … wait a moment, why would you ever want to interrupt your event loop to get a partial result? you're going to right back into the event loop anyway! So let's just use run to, well, run, our main async function which doesn't end until our program is done, and let that handle the whole shebang.

The last part was not until Python 3.7, in 2018, but run was (a) marked as "added on a provisional basis" there, and (b) started out as a shallow wrapper around "create an event loop, run run_until_complete on the coroutine you give it, then shut down the loop" which by itself isn't nearly enough to get people to update their examples.

Nowadays, asyncio.run is a whole lot more complicated and really the preferred way to run async Python code. Though admittedly my preferred way is to use anyio instead. Read https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ and https://anyio.readthedocs.io/en/stable/why.html for plenty of reasons.

1 Comment

Nice writeup, though the question was asked almost ten years ago. I wonder if it's still the case that "most" asyncio examples use loop.run_until_complete. I certainly hope not!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.