AW Dev Rethought

Code is read far more often than it is written - Guido van Rossum

🧠 Python DeepCuts — 💡 Async/Await Under the Hood


Description:

async and await are often introduced as a way to “run things in parallel,” but that mental model is misleading.

Async in Python is not about threads or CPU parallelism — it’s about how execution is scheduled.

This DeepCut explains what actually happens when you write async code and why it behaves the way it does.


🧩 What async def Really Creates

Calling an async function does not execute it.

async def greet():
    return "Hello async"

coro = greet()

Instead, Python returns a coroutine object.

This object represents pending work, not running work.

Execution begins only when the coroutine is:

  • awaited
  • scheduled by the event loop

This is why simply calling an async function does nothing by itself.


🧠 What await Actually Does

await is not a blocking operation.

await asyncio.sleep(1)

It tells the event loop:

“Pause this coroutine here, and let something else run.”

This is called cooperative multitasking:

  • tasks voluntarily yield control
  • no OS-level preemption
  • no thread switching

The event loop decides what runs next.


🔄 Coroutines vs Generators

Coroutines are implemented as state machines, similar to generators.

sample().__await__()

Internally:

  • await works by iterating over an awaitable
  • coroutines expose await()
  • execution resumes when the awaitable signals completion

This explains why async feels lightweight compared to threads.


🧠 The Event Loop Is the Scheduler

The event loop is responsible for:

  • running coroutines
  • suspending them at await points
  • resuming them when I/O is ready
await asyncio.gather(task1(), task2())

Multiple coroutines can make progress in the same time window — even on a single thread.

This is why async I/O scales well.


🧬 Async Is Not Parallelism

Async does not bypass the GIL.

async def cpu_bound():
    for i in range(10_000_000):
        pass

CPU-bound async code:

  • blocks the event loop
  • prevents other tasks from running
  • offers no speedup

Async improves throughput for I/O-bound workloads, not CPU-heavy ones.


🔍 Tasks vs Coroutines

A coroutine is just a definition of work.

A Task is a coroutine scheduled on the event loop.

task = asyncio.create_task(work())

Tasks allow the event loop to:

  • track execution
  • cancel work
  • manage lifecycle

Most frameworks automatically wrap coroutines in tasks for you.


🧠 Why Async Scales for I/O

Async excels when programs spend time:

  • waiting on networks
  • waiting on databases
  • waiting on files
  • waiting on timers

Instead of blocking threads, async:

  • suspends coroutines
  • keeps the event loop active
  • overlaps waiting periods efficiently

This is how async servers handle thousands of concurrent connections.


⚠️ Common Async Misconceptions

  • Async ≠ multithreading
  • Async ≠ multiprocessing
  • Async ≠ faster CPU execution

Async is a scheduling model, not a performance shortcut.

Understanding this distinction prevents poor architectural choices.


✅ Key Points

  • async def returns a coroutine object
  • await suspends execution cooperatively
  • The event loop schedules coroutines
  • Async runs on a single thread
  • Async is ideal for I/O-bound workloads
  • CPU-bound work still needs multiprocessing or native extensions

Async/Await is powerful — when used for the right problems.


Code Snippet:

import asyncio
import inspect

async def greet():
    return "Hello async"

coro = greet()
print(coro)
print("Is coroutine:", inspect.iscoroutine(coro))

async def task():
    print("Start task")
    await asyncio.sleep(1)
    print("Resume task")

async def one():
    await asyncio.sleep(1)
    print("One done")

async def two():
    await asyncio.sleep(1)
    print("Two done")

async def cpu_bound():
    total = 0
    for i in range(10_000_000):
        total += i
    return total

async def work():
    await asyncio.sleep(1)
    return "done"

async def main():
    await task()
    await asyncio.gather(one(), two())
    await cpu_bound()
    t = asyncio.create_task(work())
    result = await t
    print("Task result:", result)

asyncio.run(main())

Link copied!

Comments

Add Your Comment

Comment Added!