0

I am trying to read through the asyncio examples, but failed to find the simplest (in my viewpoint). Suppose I have a "normal" function that takes 1 sec. I simulate that with time.sleep(1) call. How can I wrap that function in a way that three calls would run asynchronously so the total execution time would be 1 sec?

I can do it by using threads, but not asyncio.

Here is an example:

import asyncio
import time
from threading import Thread

from datetime import datetime
from math import sqrt

def heavy_cpu(n):
    print(f"{n} --> start: {datetime.now()}")
    time.sleep(1)
    # for i in range(5099999):
    #     _ = sqrt(i) * sqrt(i)
    print(f"{n} --> finish: {datetime.now()}")

async def count(n):
    await asyncio.sleep(0.0000001)
    heavy_cpu(n)

async def main_async():
    await asyncio.gather(count(1), count(2), count(3))

def test_async():
    s = time.perf_counter()
    asyncio.run(main_async())
    elapsed = time.perf_counter() - s
    print(f"asyncio executed in {elapsed:0.2f} seconds.")

# ========== asyncio vs threading =============

def main_thread():
    threads = [Thread(target=heavy_cpu, args=(n,)) for n in range(1, 4)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

def test_thread():
    s = time.perf_counter()
    main_thread()
    elapsed = time.perf_counter() - s
    print(f"thread executed in {elapsed:0.2f} seconds.")

if __name__ == "__main__":
    test_async()
    test_thread()

The output:

1 --> start: 2020-05-12 18:28:53.513381
1 --> finish: 2020-05-12 18:28:54.517861
2 --> start: 2020-05-12 18:28:54.518162
2 --> finish: 2020-05-12 18:28:55.521757
3 --> start: 2020-05-12 18:28:55.521930
3 --> finish: 2020-05-12 18:28:56.522813
asyncio executed in 3.01 seconds.

1 --> start: 2020-05-12 18:28:56.523789
2 --> start: 2020-05-12 18:28:56.523943
3 --> start: 2020-05-12 18:28:56.524087
1 --> finish: 2020-05-12 18:28:57.5265992 --> finish: 2020-05-12 18:28:57.526689
3 --> finish: 2020-05-12 18:28:57.526849

thread executed in 1.00 seconds.

Question: why each asyncio finish step [1,2,3] takes 1 sec each? How do I make it truly async?

2 Answers 2

4

Never use time.sleep in an async program; it doesn't relinquish control to the event loop, so the event loop is blocked for the entirety of the sleep. Replace any use of time.sleep(n) with await asyncio.sleep(n), which puts that task to sleep and only requeues it when the sleep is up, allowing the event loop to do other work.

If you are in fact using time.sleep this way intentionally (you're clearly aware asyncio.sleep exists), well, that's how async works; any task that doesn't voluntarily give control back to the event loop (via await, directly or indirectly) will run to completion before any other task gets a chance to run. Asynchronous is not the same as concurrent; only async-aware activities like I/O that can be working in the background simultaneously will actually run in parallel, not normal CPU work or arbitrary blocking calls.

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

3 Comments

Well, the time.sleep is exactly what I wanted... If I simply put a computation-heavy code, there will be no difference between asyncio and threads implementations because both will end up consuming all CPU resource available so they will be as bad as sequential execution. But when I use time.sleep I get 1 sec total execution time for threading version.
@Yuri: Neither GIL-bound threads nor asyncio will let you parallelize CPU activity. Threads are preemptive multitasking (so a blocked thread can be swapped out without its cooperation), asyncio is cooperative (so they can't be swapped out without cooperation). You're asking why your tasks take a second each, serially, and it's because of time.sleep (or whatever equivalent CPU-bound code you replace it with). asyncio means you have to work harder to ensure your code execution interleaves, it never does it for you.
@Yuri: In short, if you're simulating CPU bound activity without awaits, asyncio is useless to you. It's only useful when you're I/O bound (or otherwise bound to some blocking task which is async aware, like asyncio subprocess execution or the like).
2

Asyncio is not magic. If you have a function that takes 1 second to execute because it's got a lot of calculations to do, you can't make it run three times in 1 second unless you run it on three different CPU cores. In other words, you have to use multiple processes. You say that you can make it run three times in 1 second with threads, but that's simply not true. With only one CPU core involved, it will take three seconds to run the function three times. Period. It's very simple. You need three seconds' worth of CPU time. Threads are not magic either.

Now suppose your function takes 1 second because it's spending most of its time waiting for a resource, like a network or a peripheral. Now there's a potential benefit from either threading or asyncio, depending on how the low-level functions are written. By arranging for the waits to happen in parallel you can make your function run three times in less than three seconds. That's the ONLY case when threading or asyncio makes your program go faster.

There may be other reasons to use threads or asyncio besides execution speed. In a GUI program, for example, there is typically a single thread where the GUI gets updated. If you perform a long calculation in this thread, the application will freeze until the calculation is finished. So it's often a good idea to do the calculation in a secondary thread, or in another asyncio task if your GUI platform supports that.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.