Yuri
Yuri

Reputation: 1

How to make asyncio threads in Python run in parallel?

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?

Upvotes: 0

Views: 1980

Answers (2)

Paul Cornelius
Paul Cornelius

Reputation: 10906

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.

Upvotes: 1

ShadowRanger
ShadowRanger

Reputation: 155323

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.

Upvotes: 3

Related Questions