11

I need to call a task periodically but (a)waiting times are almost more than the period.

In the following code, How can I run do_something() task without need to await for the result?

 import asyncio
 import time
 from random import randint

 period = 1  # Second


 def get_epoch_ms():
     return int(time.time() * 1000.0)


 async def do_something(name):
     print("Start  :", name, get_epoch_ms())
     try:
         # Do something which may takes more than 1 secs.
         slp = randint(1, 5)
         print("Sleep  :", name, get_epoch_ms(), slp)
         await asyncio.sleep(slp)
     except Exception as e:
         print("Error  :", e)

     print("Finish :", name, get_epoch_ms())


 async def main():
     i = 0
     while True:
         i += 1
         # Todo : this line should be change
         await do_something('T' + str(i))
         await asyncio.sleep(period)


 asyncio.get_event_loop().run_until_complete(main())
Benyamin Jafari
  • 27,880
  • 26
  • 135
  • 150
Ario
  • 549
  • 1
  • 8
  • 18
  • 2
    The time you wait for the result cannot really be shorter than the time it takes to produce that result. Can you describe the problem in more detail? – Thilo May 27 '19 at 01:30
  • yeah, sure. I am using API calls to get Cryptocurrency data from multiple websites. My goal is calling the get data API with constant period ( mean of last periods). lets assume 60 requests per min. some of the websites are lazy to answer. for example assume all the responses will deliver after 10 secs . really I don't care when the response will receive( or even it may have Timeout error after 60 secs). this waiting for API response are annoying. I just want to send requests with the same periods. – Ario May 27 '19 at 03:10
  • @Ario what if i wanted to do the exact same thing only having other lines of code running after run_until_complete, i'm looking for a solution which does not involve blocking the code with run_until_complete or asyncio.run – eran otzap Nov 20 '22 at 16:57
  • @eranotzap, when you use async tasks, tasks should wait for their result. event_loop make an environment for them and in a some point of your code you have to wait for the event_loop itself. it means the interpreter can not go further and finish the program. so we need to think about life of the event_loop. you may have better options but I'm thinking on two ways. first make and start a new thread for running event_loop and then finish your program by your other codes. second put your other codes in a function and run it as a new task( asyncio.create_task). – Ario Nov 28 '22 at 05:50
  • @Ario i came to the same conclusion, i either block at the end of the script on the main thread or block a separate thread. this also makes sense to me because of nodejs event loop which will keep your process alive (block it from ending) if something is scheduled to run – eran otzap Nov 28 '22 at 11:15

2 Answers2

10

Instead of awaiting the coroutine, call asyncio.create_task to spawn a task object that runs in the background. At your next iteration you can check if the task is done and await/cancel it accordingly. (Otherwise asyncio will complain of un-awaited tasks being garbage collected.)

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • well, that's until you get an error "coroutine '***' was never awaited" – carlo Aug 13 '23 at 19:53
  • @carlo Can you give an example? I don't think you should be getting this warning if you create a task out of the coroutine, and either await or cancel it, as the answer suggests. – user4815162342 Aug 14 '23 at 06:39
  • yeah that's true. I'm a bit struggling with asyncio as for now, because I don't like that the program/thread must keep running after the task is created, but I'm getting the hang of it now. – carlo Aug 14 '23 at 11:35
5

Your problem is using the run_until_complete(main()) which does not satisfy your concurrency purpose. So, assuming your coroutine tasks (do_something()) are bounded to 5, your code will be as follows:

import time
from random import randint

period = 1  # Second

def get_epoch_ms():
    return int(time.time() * 1000.0)

async def do_something(name):
    print("Start  :", name, get_epoch_ms())
    try:
        # Do something which may takes more than 1 secs.
        slp = randint(1, 5)
        print("Sleep  :", name, get_epoch_ms(), slp)
        await asyncio.sleep(slp)
    except Exception as e:
        print("Error  :", e)

    print("Finish :", name, get_epoch_ms())

loop = asyncio.get_event_loop()
futures = [loop.create_task(do_something('T' + str(i)))
           for i in range(5)]

loop.run_forever()

for f in futures:
    f.cancel()

Here is the concurrency workflow in its output:

Start  : T0 1558937750705
Sleep  : T0 1558937750705 5
Start  : T1 1558937750705
Sleep  : T1 1558937750705 1
Start  : T2 1558937750705
Sleep  : T2 1558937750705 4
Start  : T3 1558937750705
Sleep  : T3 1558937750705 5
Start  : T4 1558937750705
Sleep  : T4 1558937750705 5
Finish : T1 1558937751707
Finish : T2 1558937754709
Finish : T0 1558937755707
Finish : T3 1558937755708
Finish : T4 1558937755708

However, If your coroutine tasks are not bounded you could do it:

...
async def main(loop):
    i = 0
    while True:
        i += 1
        loop.create_task(do_something('T' + str(i)))
        await asyncio.sleep(period)

loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
Benyamin Jafari
  • 27,880
  • 26
  • 135
  • 150
  • loop.run_forever() never yields execution back to the python interpreter so all though the task run in the background the code blocks, how can i runs a task which uses async await without having to block the initiating code ? – eran otzap Nov 19 '22 at 22:08
  • @eranotzap in the above example I used `run_forever()`, however the tasks are bounded – Benyamin Jafari Nov 20 '22 at 07:27
  • run_forever blocks the executing thread, i'm looking for a way to send a task to the event loop without blocking execution – eran otzap Nov 20 '22 at 17:01
  • @eranotzap yes, because asyncio tasks are being run in a single thread. So if you really want something that not to be blocking you most likely need the combination of threading and asyncio. I mean call the asyncio runner in a sub-thread. – Benyamin Jafari Nov 21 '22 at 06:07
  • that's not what i meant, i know it's a single thread on the single event loop it just that starting the event loop blocks from main thread blocks the main thread. what i ended up doing is running the calling run_forever at the very end of my script – eran otzap Nov 21 '22 at 10:13