8

If I have some function, which does a lot of calculations, and it can take a while, is it good to use asyncio.sleep() between the parts of calculations to release event loop (to prevent blocking event loop)?

import asyncio


async def long_function(a, b, c):
    # some calculations
    await asyncio.sleep(0)  # release event loop
    # some another calculations
    await asyncio.sleep(0)  # release event loop

Is there another, more better way to solve such problems? Some best practices, maybe?

Vladimir Chub
  • 461
  • 6
  • 19
  • Possible solution: https://stackoverflow.com/a/33399896/476 – deceze Dec 13 '19 at 07:31
  • @deceze Thank you for your comment! As I see, the author of answer you referenced advises to use ProcessPoolExecutor/ThreadPoolExecutor to transform synchronous code to asynchronous. But I ask little different question: what is the best solution to divide already asynchronous function to multiple sub-functions to prevent blocking event loop for a time of this function takes for calculations? Is asyncio.sleep() good for this? – Vladimir Chub Dec 13 '19 at 07:46
  • 2
    It is not a good idea overall to run compute intensive tasks in an event loop. As mentioned above use a process pool to run compute intensive tasks. – cedzz Dec 13 '19 at 07:53
  • 3
    You're asking how to run a long running function without blocking other async functions. The best way to do that is to run it in an executor. The next best way would probably be to chop it up into smaller async tasks which "naturally break". I would probably never want to insert explicit `sleep` calls somewhere just to yield to other async code. – deceze Dec 13 '19 at 07:53
  • Thank you all for your comments! I worry that you didn't post answers, and I can't vote. – Vladimir Chub Dec 13 '19 at 08:02
  • 2
    *what is the best solution to divide already asynchronous function to multiple sub-functions to prevent blocking event loop for a time of this function takes for calculations* - The answer is: a function that takes a long time to calculate something is not "already asynchronous", at least not in the way useful for asyncio. Adding `await asyncio.sleep(0)` is somewhat of a hack, you should rather move the calculation to an off-thread as shown in other comments. – user4815162342 Dec 13 '19 at 09:12

1 Answers1

11

TL;DR just use loop.run_in_executor to do blocking work


To understand why it doesn't help, let's first make a class that does something with the event loop. Like:

class CounterTask(object):
    def __init__(self):
        self.total = 0
    
    async def count(self):
        while True:
            try:
                self.total += 1
                await asyncio.sleep(0.001)  # Count ~1000 times a second
            except asyncio.CancelledError:
                return

This will simply count around 1000 times a second, if the event loop is completely open to it.

Naive

Just to demonstrate the worst way, let's start the counter task and naively run an expensive function without any thought to the consequences:

async def long_function1():
    time.sleep(0.2)  # some calculations


async def no_awaiting():
    counter = CounterTask()
    task = asyncio.create_task(counter.count())
    await long_function1()
    task.cancel()
    print("Counted to:", counter.total)


asyncio.run(no_awaiting())

Output:

Counted to: 0

Well that didn't do any counting! Notice, we never awaited at all. This function is just doing synchronous blocking work. If the counter was able to run in the event loop by itself we should have counted to about 200 in that time. Hmm, so maybe if we split it up and leverage asyncio to give control back to the event loop it can count? Let's try that...

Splitting it up

async def long_function2():
    time.sleep(0.1)  # some calculations
    await asyncio.sleep(0)  # release event loop
    time.sleep(0.1)  # some another calculations
    await asyncio.sleep(0)  # release event loop


async def with_awaiting():
    counter = CounterTask()
    task = asyncio.create_task(counter.count())
    await long_function2()
    task.cancel()
    print("Counted to:", counter.total)


asyncio.run(with_awaiting())

Output:

Counted to: 1

Well I guess that's technically better. But ultimately this shows the point: The asyncio event loop shouldn't do any blocking processing. It is not intended to solve those issues. The event loop is helplessly waiting for your next await. But the run_in_executor does provide a solution for this, while keeping our code in the asyncio style.

Executor

def long_function3():
    time.sleep(0.2)  # some calculations


async def in_executor():
    counter = CounterTask()
    task = asyncio.create_task(counter.count())
    await asyncio.get_running_loop().run_in_executor(None, long_function3)
    task.cancel()
    print("Counted to:", counter.total)


asyncio.run(in_executor())

Output:

Counted to: 164

Much better! Our loop was able to continue going while our blocking function was doing things as well, by the good old-fashion way of threads.

Dima
  • 35
  • 5
ParkerD
  • 1,214
  • 11
  • 18
  • Thank you for your answer! I always thought previously, that `run_in_executor` is using as bridge/adapter only: for porting synchronous libraries to asyncio. It never occurred to me that it should be used in the functions which use CPU-bound operations as a part of regular development flow in asyncio. – Vladimir Chub Dec 16 '19 at 03:59