6

So i have a script for a countdown which looks something like this:

import time, threading, asyncio
def countdown(n, m):
    print("timer start")
    time.sleep(n)
    print("timer stop")
    yield coro1

async def coro1():
    print("coroutine called")

async def coromain():
    print("first")
    t1 = threading.Thread(target=countdown, args=(5, 0))
    t1.start()
    print("second")

loop = asyncio.get_event_loop()
loop.run_until_complete(coromain())
loop.stop()

What i want it to do is simple:

Run coromain
Print "first"
Start thread t1, print "timer start" and have it wait for 5 seconds
In the mean time, print "second"
after 5 seconds, print "timer stop"
exit

However, when i run this code it outputs:

Run coromain
Print "first"
Print "second"
exit

I'm so confused as to why it does this. Can anyone explain what I'm doing wrong here?

2
  • 1
    Can you please explain the choice of running the countdown in a different thread? Commented Feb 26, 2018 at 20:06
  • i'm making a discord bot. I need it to count down for n seconds and then notify the user, while still being able to accept commands in the main thread Commented Feb 26, 2018 at 20:53

3 Answers 3

6

This depends on whether your question is a part of a bigger problem imposing additional constraints or not, but I do not see a reason to use threading. Instead, you can use two separate Tasks running in the same event loop, which is one of the main points of asynchronous programming:

import asyncio

async def countdown(n, m):  # <- coroutine function
    print("timer start")
    await asyncio.sleep(n)
    print("timer stop")
    await coro1()

async def coro1():
    print("coroutine called")

async def coromain():
    print("first")
    asyncio.ensure_future(countdown(5, 0))  # create a new Task
    print("second")

loop = asyncio.get_event_loop()
loop.run_until_complete(coromain())  # run coromain() from sync code
pending = asyncio.Task.all_tasks()  # get all pending tasks
loop.run_until_complete(asyncio.gather(*pending))  # wait for tasks to finish normally

Output:

first
second
timer start
(5 second wait)
timer stop
coroutine called

When using ensure_future, you effectively make a new “execution thread” (see fibers) inside the single OS's thread.

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

6 Comments

Awesome, i'm not that familiar with asyncio, but was forced upon it because I'm using a library that only works with said library. I thought there had to be a way like this, but couldn't make it out from the docs
You might want to use create_task instead of ensure_future when you pass it an actual coroutine. According to Guido, ensure_future is a only needed when you are converting something that may or may not be a future and need an actual Future result, e.g. to call cancel or add_done_callback on it.
@user4815162342 That's an interesting point, thanks. I usually use ensure_future because I rarely have an explicit loop lying around.
That makes sense, but does have ensure_future have any overhead that create_task doesn't have? Aka, would it be better to use create_task when you know that you are dealing with a Future?
@DavinMiler In this case it's not about the overhead, it's about clarity of code. ensure_future indicates that the code is trying to convert some object into a future. create_task always accepts a coroutine and returns a task, which is a subclass of Future that "drives" a coroutine in the event loop. Now, ensure_future is smart enough to return a task when given a coroutine, but using create_task more precisely indicates the intent of the code.
|
0

After some digging crafted this workaround. It might not be pretty, but it works:

import time, threading, asyncio
def countdown(n, m):
    print("timer start")
    time.sleep(n)
    print("timer stop")
    looptemp = asyncio.new_event_loop()
    asyncio.set_event_loop(looptemp)
    loop2 = asyncio.get_event_loop()
    loop2.run_until_complete(coro1())
    loop2.close()

async def coro1():
    print("coroutine called")

async def coromain():
    print("first")
    t1 = threading.Thread(target=countdown, args=(5, 0))
    t1.start()
    print("second")

loop = asyncio.get_event_loop()
loop.run_until_complete(coromain())
loop.stop()

It unfortunately doesn't work for my specific usecase, but I thought it might be useful.

Comments

0

I'm so confused as to why it does this. Can anyone explain what I'm doing wrong here?

There is already an accepted answer showing how to achieve what you want, but just to explain why your code produced the output it did:

The coroutine coromain starts the countdown as a thread, but does not wait for it to finish. Actually your coroutine, and therefore your program, exits before the thread could be exectued.

Comments