Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it good to use asyncio.sleep() in long running code to divide async function to multiple smaller parts of code?

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?

like image 863
Vladimir Chub Avatar asked Dec 13 '19 07:12

Vladimir Chub


People also ask

How many times should Asyncio run () be called?

How many times should Asyncio run () be called? It should be used as a main entry point for asyncio programs, and should ideally only be called once. New in version 3.7.

How do I run two async functions forever?

To do so we have to create a new async function (main) and call all the async functions (which we want to run at the same time) in that new function (main). And then call the new (main) function using Event Loops… Code: Python3.

Does Asyncio use multiple cores?

However, to my understanding, Async/IO means the server can only run on one processing core. Regular, synchronous servers like uwsgi , on the other hand, can fully utilize the computer's computing resources with truly parallel threads and processes.

What is Asyncio sleep?

What is Asyncio sleep? The Sleep() Function Of Asyncio In Python The asyncio. sleep() method suspends the execution of a coroutine. Coroutines voluntarily yield CPU leading to co-operative multitasking through the await keyword.


1 Answers

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.

like image 108
ParkerD Avatar answered Nov 01 '22 05:11

ParkerD