Chapter 17: Doing Two Things at Once (Concurrency & Async)
Learning Objectives
- Understand the difference between CPU-bound and I/O-bound tasks.
- Learn why Synchronous code fails on the modern web.
- Master the concepts of Async / Await and Event Loops.
- Understand the danger of Race Conditions.
Prerequisites
Chapter 16: APIs (I/O Wait Times).
Why Does This Exist?
By default, code executes exactly one line at a time. It is Synchronous.
If Line 5 is download_10gb_movie(), the CPU will stop on Line 5 for an hour. It will not execute Line 6. If Line 6 was update_progress_bar(), the progress bar will never update. The user's screen will freeze. The OS will display a spinning beachball and ask the user to force quit.
We needed a way to tell the CPU: "Start this long download, but don't wait here. Go do other things (like updating the UI), and come back when the download is finished."
We needed Concurrency.
History
Historically, engineers achieved this using Threads. You would tell the OS to split your program into two parallel execution lanes. Thread 1 downloads the file, Thread 2 runs the UI.
Threading is incredibly difficult. If two threads try to modify the same variable at the exact same millisecond, the data corrupts. Debugging threads drove engineers insane.
Recently, languages like JavaScript and Python popularized Async/Await and the Event Loop. It provides the illusion of threading but safely runs on a single thread, eliminating 90% of the complexity.
Mental Model
Synchronous: A bad chef. He puts water on the stove to boil. He crosses his arms and stares at the pot for 10 minutes. He does absolutely nothing else. When it boils, he finally starts chopping onions.
Asynchronous (Event Loop): A master chef. He puts water on the stove, sets a timer (a callback), and immediately starts chopping onions. When the timer rings (the Event), he pauses chopping, drops pasta in the water, and goes back to chopping.
He is only doing one thing at any exact millisecond (Single Threaded), but he is juggling tasks so efficiently that it looks like he is doing everything at once.
Internal Working
The Event Loop is a literal while True: loop hidden deep inside the language runtime.
When you call an await function (like fetching an API), the CPU sends the network request to the Operating System's background hardware.
The CPU then says: "I yield my time." The Event Loop instantly jumps to another part of your code that is ready to run.
When the network card receives the data 200ms later, the OS puts a message in the "Event Queue". The next time the Event Loop circles around, it sees the message, picks up the data, and resumes the original function exactly where it left off.
Syntax
In modern Python (and JS/C#), we use async def and await.
1import asyncio
2
3async def fetch_data():
4 print("1. Requesting data...")
5 # 'await' tells the Event Loop to pause THIS function and go do other stuff.
6 await asyncio.sleep(2) # Simulating a slow 2-second network call
7 print("3. Data received!")
8
9async def do_ui_work():
10 print("2. Updating animations while waiting...")
11
12async def main():
13 # asyncio.gather runs both functions concurrently
14 await asyncio.gather(fetch_data(), do_ui_work())
15
16asyncio.run(main())
Output Order: 1, then 2 (instantly), then 3 (two seconds later).
Visual Explanation
Common Mistakes
The Blocking Function in an Async Loop
1async def bad_idea():
2 print("Start")
3 time.sleep(5) # BAD: A synchronous sleep!
4 print("End")
Why it fails: time.sleep() (or raw math like while True:) is CPU-blocking. It does NOT yield control back to the Event Loop. You have completely paralyzed the master chef. The entire server freezes for 5 seconds.
The Fix: Inside async def functions, you can ONLY use other asynchronous tools (like await asyncio.sleep()) or non-blocking I/O libraries.
Debugging
Concurrency bugs are the hardest bugs in computer science. They are non-deterministic (they only happen sometimes, depending on exactly which microsecond the CPU switched tasks).
A Race Condition happens when Task A and Task B both try to read and update a bank balance at the exact same time. The math overwrites itself, and money vanishes.
To fix this, engineers use Locks/Mutexes to force a bottleneck: "Only one task is allowed to touch this specific variable at a time."
Mini Project
Time: 20 minutes.
Goal: The Race.
Write an async function download(file_name, delay) that prints "Started [name]", awaits for delay seconds, and prints "Finished [name]". Use asyncio.gather to start 3 downloads simultaneously with random delays between 1 and 3 seconds. Watch them finish in random order based on their delay, while the total program time is only 3 seconds, not 6!
Bigger Project
Time: 1.5 hours.
Goal: High-Speed API Scraper.
Use the aiohttp library (the async version of requests). You have a list of 50 Pokemon URLs. First, write a standard for loop using normal requests to fetch all 50. Time it (it will take ~15 seconds). Next, write an async version that triggers all 50 fetches concurrently using asyncio.gather. Time it (it will take ~1 second!).
Production Usage
Node.js revolutionized backend web development because it is built entirely around an asynchronous Event Loop.
If you build a chat app in a synchronous framework, a server with 1,000 users needs 1,000 OS Threads just to keep the connections open, which consumes gigabytes of RAM. In an async framework, a single thread can keep 10,000 connections open concurrently, using almost no RAM, because 99% of a chat app's life is just waiting for network packets.
Best Practices
- CPU-Bound vs I/O-Bound: Use Async for I/O-Bound tasks (Network requests, Database queries, File reading). If your task is CPU-Bound (Calculating Pi to a billion digits, rendering 3D graphics), Async will not help you. You must use true Multi-Processing to utilize multiple physical CPU cores.
Interview Questions
Easy: What does "Blocking" mean?
Answer: Code that halts the entire thread of execution until it finishes, preventing any other code from running.
Medium: Explain the difference between Concurrency and Parallelism.
Answer: Concurrency is juggling (managing multiple tasks by switching between them rapidly on a single core). Parallelism is actually having two hands (executing tasks at the exact same time on multiple physical CPU cores).
Hard: What is a Deadlock?
Answer: When Task A locks Resource 1 and waits for Resource 2, but Task B has locked Resource 2 and is waiting for Resource 1. Neither can proceed. They wait for each other forever, freezing the program permanently.
Revision Sheet
- Synchronous: One line at a time. Blocks on slow operations.
- Asynchronous: Yields control during slow operations to do other work.
- Event Loop: The central engine that tracks paused tasks and resumes them when their data arrives.
- await: The keyword that tells the Event Loop, "Pause me here, I am waiting for I/O."
Connections
- Previous Chapter: You MUST use async when making API requests (Chapter 16) in a UI application, or the UI will freeze.
- Future Chapters: With async, our code is getting complex and hard to predict. How do we ensure we didn't break anything? Automated Testing (Chapter 18).