Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does asyncio.sleep(0) make my code faster?

If I delete this line from my scanner,

await asyncio.sleep(0)

Work time grows from 5s to 400s.

Why is this happening?

My code:

import os
import asyncio
import time

async def rl(response):
    await asyncio.sleep(0)
    return response.readlines()  

async def scan_Ip(addr):
    print(addr)
    response = os.popen("ping -n 1 " + addr)
    data = await rl(response)
    for line in data:
        if 'TTL' in line:
            print(data)

async def scan():
    tasks=[]
    for ip in range(0, 256):
        tasks.append(asyncio.create_task(scan_Ip(f'192.168.8.{ip}')))
    await asyncio.wait(tasks)

if __name__ == '__main__':
    start_time = time.time()
    asyncio.run(scan())
    print(f"--- {time.time() - start_time} seconds ---")
like image 657
Anton Avatar asked Dec 03 '25 16:12

Anton


2 Answers

As pointed out by Nullman in a comment on the question, os.popen doesn't provide an async interface, so using it from asyncio makes the coroutine that does so block the event loop. A consequence of that is that multiple such coroutines execute sequentially regardless of the use of gather or wait.

Notice how your rl function reads data with a blocking readlines() call and doesn't actually await anything (other than sleep(0)). That is a reliable indicator that rl is not async except in name and that it will cause a bottleneck once you try to run it in parallel.

The correct way to interface with ping from asyncio is using the asyncio equivalent of os.popen:

async def scan_Ip(addr):
    print(addr)
    proc = await asyncio.create_subprocess_exec(
        "ping", "-n", "1", addr, stdout=subprocess.PIPE
    )
    async for line in proc.stdout:
        if 'TTL' in line:
            print(data)

Now process interaction is performed with async calls, which allows performing them in parallel for all 256 processes.

As for why await asyncio.sleep(0) helps - awaiting asyncio.sleep() forces the async function to yield control to the event loop (even when the delay is 0). This allows another coroutine to execute up to subsequent await, and so on, and ends up affecting execution order in a way that is beneficial to this program. Without asyncio.sleep(0) the scan_Ip/rl pair never drop to the event loop and are executed in sequence, like this (simplified):

  1. x1 = os.popen("ping -n 1 192.168.8.0")
  2. for line in x1.readlines(): ...
  3. x2 = os.popen("ping -n 1 192.168.8.1")
  4. for line in x2.readlines(): ...
...
511. x256 = os.popen("ping -n 1 192.168.8.255")
512. for line in x256.readlines(): ...

Every ping is started, fully processed, and stopped, before another is even considered. With an await asyncio.sleep(0) between os.popen() and readlines(), the code is not fully async yet, but it does add a switch between the two steps, resulting in this order of execution:

  1. x1 = os.popen("ping -n 1 192.168.8.0")
  2. x2 = os.popen("ping -n 1 192.168.8.1")
...
256. x256 = os.popen("ping -n 1 192.168.8.255")
257. for line in x1.readlines(): ...
258. for line in x2.readlines(): ...
...
512. for line in x256.readlines(): ...

In other words, the same steps are executed, but reordered so that first all the commands are started, and only then are they read from. That allows the ping commands to produce their output (and connect to the network, etc.) all in parallel. The communication step then just picks up the output that is basically ready, lying in the pipe buffer. The fact that all pings executed in parallel is what makes that version faster.

In closing, note that idiomatic asyncio code, as the one that uses asyncio.subprocess, doesn't require asyncio.sleep(0) because it already consistently awaits when waiting for data, and forcing additional sleeps would in fact make it slower.

like image 100
user4815162342 Avatar answered Dec 06 '25 10:12

user4815162342


After running the code, it in fact does not increase runtime at all on my machine (or by only minimal times). I would assume that this is somehow triggering something similar to a ratelimit (a very short one) and having to retry sometimes, effectively doubling the runtime. Your machine might be different, but this is what I got on MacOS Catalina.

With asyncio.sleep(0): 1.9657979011535645 seconds

Without asyncio.sleep(0): 3.2927019596099854 seconds

like image 23
BobDotCom Avatar answered Dec 06 '25 11:12

BobDotCom



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!