Async & Concurrency — Overview
Coming from JavaScript’s single-threaded world
Section titled “Coming from JavaScript’s single-threaded world”JavaScript runs in a single thread. There is one call stack, one event loop, and no shared memory between concurrent units of work. When you write async/await in Node.js you are scheduling microtasks on that single thread — you never run two things truly at the same time inside one process.
Python gives you three different concurrency tools and you have to choose the right one for the job:
| Tool | What it is | Best for |
|---|---|---|
asyncio | Single-threaded cooperative concurrency | I/O-bound async code (HTTP, DB, files) |
threading | OS threads sharing one interpreter | I/O-bound code, legacy blocking libraries |
multiprocessing | Separate OS processes | CPU-bound work that needs true parallelism |
Concurrency vs parallelism
Section titled “Concurrency vs parallelism”These two words are often conflated. They mean different things:
- Concurrency — multiple tasks are in progress at the same time, but not necessarily running at the exact same instant. The event loop switches between tasks when one is waiting. This is how Node.js works, and how Python’s
asyncioworks. - Parallelism — multiple tasks execute at the exact same instant on different CPU cores. This requires multiple OS processes in Python (or multiple threads in most other languages).
// JavaScript/Node.js// Concurrency via the event loop — single thread, never truly parallelasync function fetchUser(id: number): Promise<User> { // While this awaits, other promises can run return await db.query("SELECT * FROM users WHERE id = $1", [id]);}
// Promise.all is concurrent, not parallel — still one threadconst [user, posts] = await Promise.all([ fetchUser(1), fetchPosts(1),]);# Python# asyncio — same single-threaded cooperative model as JSimport asyncio
async def fetch_user(user_id: int) -> dict: # While this awaits, other coroutines can run await asyncio.sleep(0) # yield to event loop return {"id": user_id}
# asyncio.gather — same idea as Promise.allasync def main() -> None: user, posts = await asyncio.gather( fetch_user(1), fetch_posts(1), )The three layers at a glance
Section titled “The three layers at a glance”// JavaScript has one model: event loop + Web Workers for threads// One thread for JS logic, workers for CPU work (separate memory)
// Main threadconst worker = new Worker("worker.js");worker.postMessage({ data: bigArray });worker.onmessage = (e) => console.log(e.data);# Python has three modelsimport asyncio # like Node's event loopimport threading # OS threads (limited by GIL — see below)import multiprocessing # true parallelism, separate memory
# Pick based on your bottleneck:# waiting on network/disk → asyncio or threading# heavy CPU math → multiprocessingWhich tool should I reach for?
Section titled “Which tool should I reach for?”flowchart TD
io{"Bottleneck is waiting on I/O? (network, disk, DB)"}
cpu{"Bottleneck is CPU computation? (math, image, ML)"}
simple{"Just want a simple high-level API?"}
io -- Yes --> asyncio["Use asyncio (preferred) or threading"]
io -- No --> cpu
cpu -- Yes --> mp["Use multiprocessing (bypasses the GIL)"]
cpu -- No --> simple
simple -- Yes --> cf["Use concurrent.futures (wraps threading and multiprocessing)"] The rest of this module covers each tool in depth.