Concurrency
Master Python's concurrency models and learn when to use threads, processes, or AsyncIO for maximum performance.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • The difference between concurrency and parallelism — and why it matters
- • How Python threads work and when the GIL limits you
- • When to use threading vs multiprocessing vs asyncio
- • How to share data safely between threads using locks and queues
- • How to spawn and manage worker processes for CPU-bound tasks
- • Real-world patterns: scrapers, batch processors, concurrent file downloads
🔥 1. Why Concurrency Exists
- CPU cores → true parallelism (multiple chefs)
- Scheduling → switching between tasks (one chef, many pots)
- I/O waits → idle time you can use productively (waiting for water to boil)
Type of Work
What It Means
Best Solution
Real Example
CPU-bound
Heavy calculations that keep the CPU busy
multiprocessing
Image processing, ML training
I/O-bound
Waiting for external resources
threads or asyncio
API calls, file downloads
⚙️ 2. Threads in Python
A thread is a lightweight unit of execution within a single process.
What Threads Share
Why It Matters
Memory
Fast communication, but risk of conflicts
Variables
Easy data sharing, but need locks for safety
File handles
Can work with same files simultaneously
Python interpreter
Limited by GIL for CPU work
- network requests (waiting for servers)
- reading/writing files (waiting for disk)
- user interface responsiveness (don't freeze the UI)
- downloading many URLs (lots of waiting)
- heavy computation (GIL blocks parallelism)
- CPU-bound workload (use processes instead)
🧠 3. Understanding the GIL (Global Interpreter Lock)
The GIL (Global Interpreter Lock) is a mutex that protects access to Python objects.
Situation
GIL Effect
Result
Running Python code
GIL is held
Other threads wait
Waiting for network/file
GIL is released
Other threads can run
Using C extensions (NumPy)
Often released
True parallelism possible
(e.g., image processing, hashing, compression)
⚡ 4. Processes in Python
A process is a full Python interpreter with its own memory.
Aspect
Threads
Processes
Shared
Separate (isolated)
GIL
Shared (limits CPU work)
Each has its own
Startup time
Fast (microseconds)
Slow (milliseconds)
Data sharing
Easy (same memory)
Requires serialization
- True parallelism (uses multiple CPU cores)
- Great for CPU-bound work
- No GIL problems — each process has its own
- More memory used (each process needs its own)
- Slower to start (spawning a new Python interpreter)
- Harder to share data (must serialize/deserialize)
🔄 5. Side-by-Side Comparison
Feature
Speed for I/O
⭐⭐⭐⭐⭐
⭐⭐⭐
Speed for CPU
⭐⭐
Memory usage
Low
High
Fast
Slow
Shares memory?
Yes
No
Avoids GIL?
❌
✔
Best for?
I/O tasks
CPU tasks
🧪 6. Real-World Examples
Threads shine because requests are I/O-bound.
🧱 7. Mixing Threads & Processes (Hybrid Model)
- Processes for CPU-heavy pipelines
- Threads for network/database operations
- AsyncIO for massive lightweight tasks
Component
Role in Hybrid System
Why?
AsyncIO
Orchestra conductor
Lightweight coordination of thousands of tasks
Threads
I/O specialists
Handle blocking I/O without stopping AsyncIO
Processes
Heavy lifters
CPU work on multiple cores simultaneously
- Threads → fetch 1,000 URLs
- Processes → process each page's text
- AsyncIO → coordinate flows
This is how production-grade scrapers, ML preprocessors, and automation bots work.
🔥 8. When Should You Use What?
- ✔ Web scraping
- ✔ Automation
- ✔ Network servers
- ✔ Waiting on APIs
- ✔ ML preprocessing
- ✔ Math-heavy operations
- ✔ Hashing, encryption
- ✔ Image/video processing
- ✔ Massive concurrency
- ✔ Lightweight operations
- ✔ Network-first tasks
- ✔ High scalability needed
🧠 9. How Python Schedules Threads Internally (Advanced)
Python uses cooperative + preemptive scheduling for threads.
The operating system decides when each thread runs, based on CPU availability.
Inside Python, only ONE thread can execute Python bytecode at once.
- artificial bottlenecks for CPU tasks
- no bottlenecks for I/O (because threads release the GIL while waiting)
- A thread starts running Python code
- When it hits I/O (network, file), it releases the GIL
- Another thread can run
- When I/O completes, the thread resumes and reacquires GIL
- ✔ 100 threads downloading files works great
- ❌ 100 threads crunching numbers does NOT
⚡ 10. The True Strength of Threads: I/O Parallelism
Let's say you need to download 10,000 images.
Sequential time = 10,000 × (0.4 seconds each) = ~4000 seconds (1.1 hours)
Threaded time (200 threads) = ~20 seconds total
Because threads wait most of the time, so Python overlaps waits.
- HTTP requests
- reading/writing files
- database queries
- waiting for user input
- network sockets
🔥 11. The True Strength of Processes: CPU Parallelism
- ML preprocessing
- physics simulation
- number crunching
- image/video filtering
- audio processing
If you run these in threads → NO speed improvement .
If you run these in processes → 4× faster on 4 cores, 12× on 12 cores, etc.
🧬 12-18. Advanced Concurrency Topics
The next sections cover professional-level concurrency patterns:
Lock, RLock, Event, Semaphore, Queue — Safe shared memory primitives
multiprocessing.Queue, Pipe, Manager, shared memory arrays
14. ThreadPoolExecutor vs ProcessPoolExecutor
concurrent.futures abstraction for both models
Combining AsyncIO + Threads + Processes like YouTube/Instagram
Race conditions, deadlocks, blocking calls, serialization issues
CPU-heavy: 8× with processes; I/O-heavy: 50× with threads
✔ Threads for I/O | ✔ Processes for CPU | ✔ AsyncIO for massive concurrency
🧨 19. Race Conditions — The Silent Killer
- Two or more threads access shared data
- AND at least one thread modifies it
- AND execution order determines the final result
Symptom
What's Happening
Example
Inconsistent results
Different output each run
Counter shows 987,432 instead of 1,000,000
Lost updates
Changes disappear
Two users edit same record, one is lost
Works sometimes
Timing-dependent bugs
Passes tests locally, fails in production
counter += 1 is NOT atomic. It's 3 instructions:
- load counter
- store result
🧱 20. Fixing Race Conditions With Locks
Locks ensure only ONE thread accesses critical code at once.
Lock Method
What It Does
When to Use
lock.acquire()
Grab the lock (waits if taken)
Manual control needed
lock.release()
Release the lock
After acquire()
with lock:
Auto acquire + release
✅ Always prefer this!
- ✔ Deterministic
- ✔ Correct final value
But… ❗ Locks introduce blocking, which could slow threads.
🔒 21-32. Expert-Level Concurrency Patterns
When threads freeze forever waiting on each other
What breaks when passing objects to processes
29. Real Architecture: High-Performance Scraper
ProcessPool for CPU, ThreadPool for I/O, AsyncIO for APIs
Python 3.13+ will enable true parallel threads for CPU work
🎓 Final Summary
You're operating at professional backend engineer level now.
📋 Quick Reference — Concurrency
Tool
Best for
threading.Thread
I/O-bound tasks, network calls
multiprocessing.Process
CPU-bound tasks (bypasses GIL)
threading.Lock()
Protect shared state between threads
queue.Queue()
Thread-safe data passing
multiprocessing.Queue()
Process-safe data passing
You now understand the GIL, when to use threads vs processes, and how to safely share data between concurrent workers.
Up next: Parallelism — use concurrent.futures for a clean high-level API over threads and processes.
Practice quiz
What is the difference between concurrency and parallelism?
- They are the same thing
- Parallelism is slower than concurrency
- Concurrency switches between tasks; parallelism does multiple things at the exact same time
- Concurrency requires multiple CPU cores
Answer: Concurrency switches between tasks; parallelism does multiple things at the exact same time. Concurrency interleaves tasks; parallelism runs them truly simultaneously on multiple cores.
What is the GIL (Global Interpreter Lock)?
- A mutex that lets only one thread run Python bytecode at a time
- A networking protocol
- A way to lock files
- A garbage collector
Answer: A mutex that lets only one thread run Python bytecode at a time. The GIL is a mutex ensuring only one thread executes Python bytecode at a time.
For CPU-bound work, which approach actually achieves true parallelism in Python?
- threading
- asyncio
- None — Python can't parallelize
- multiprocessing
Answer: multiprocessing. Each process has its own interpreter and GIL, so multiprocessing gives true CPU parallelism.
Why do threads work well for I/O-bound tasks despite the GIL?
- The GIL is disabled for threads
- A thread releases the GIL while waiting on I/O, letting another thread run
- I/O tasks don't use the GIL at all
- Threads create separate interpreters
Answer: A thread releases the GIL while waiting on I/O, letting another thread run. When a thread waits on I/O it releases the GIL, so other threads can make progress.
Do threads or processes share memory by default?
- Threads share memory; processes have separate memory
- Processes share memory; threads do not
- Both share memory
- Neither shares memory
Answer: Threads share memory; processes have separate memory. Threads share the same memory; processes are isolated and need serialization to share data.
Why is counter += 1 unsafe across multiple threads without a lock?
- It is always atomic and safe
- Integers can't be shared
- It is not atomic — load, add, and store can interleave
- The GIL prevents all sharing
Answer: It is not atomic — load, add, and store can interleave. counter += 1 is three steps (load, add, store); interleaving threads corrupt the value — a race condition.
What is the recommended way to use a threading.Lock around shared data?
- lock.acquire() and forget to release
- with lock:
- Set lock = True
- No lock is needed
Answer: with lock:. with lock: auto-acquires and auto-releases, which is the safest pattern.
Compared with threads, how do processes generally start up?
- Faster, in microseconds
- Instantly with zero cost
- At the same speed as threads
- Slower, in milliseconds (a new interpreter must spawn)
Answer: Slower, in milliseconds (a new interpreter must spawn). Processes are slower to start since each spawns its own Python interpreter and memory.
Which model is best for handling massive numbers of lightweight network tasks?
- multiprocessing
- asyncio
- One thread per task
- Pure sequential code
Answer: asyncio. asyncio scales to massive lightweight, network-first concurrency on a single thread.
Running heavy number-crunching in 100 threads gives what result in CPython?
- A ~100x speedup
- A guaranteed crash
- No real speedup — the GIL serializes CPU-bound bytecode
- True parallelism across cores
Answer: No real speedup — the GIL serializes CPU-bound bytecode. The GIL serializes Python bytecode, so CPU-bound threads get no real speedup — use processes.