Python Multithreading — Core Concepts
Multithreading means running multiple threads of execution within one process. In Python, it is primarily a tool for I/O concurrency and application responsiveness.
Why Use Threads
Threads are useful when tasks block on external waits:
- network requests
- database calls
- file I/O
- user input / GUI waits
If one thread blocks, others can continue. This often reduces total wall-clock time for mixed I/O workloads.
Basic Thread Lifecycle
import threading
def task(n):
print(f"Task {n}")
t = threading.Thread(target=task, args=(1,))
t.start()
t.join()
start()schedules thread executionjoin()waits until it finishes
The GIL in Practice
Python’s Global Interpreter Lock (GIL) allows only one thread to execute Python bytecode at a time per process (in CPython). This means:
- CPU-bound pure Python threads do not scale linearly with core count
- I/O-bound workloads still benefit because blocked threads release GIL during I/O waits
So thread strategy depends on bottleneck type.
Shared State and Race Conditions
Race condition: outcome depends on timing between threads.
counter = 0
def inc():
global counter
for _ in range(100_000):
counter += 1
Multiple threads running inc can lose updates.
Locking
lock = threading.Lock()
with lock:
counter += 1
Lock critical sections that mutate shared data.
Synchronization Toolbox
Python threading offers primitives for different coordination problems:
Lock: mutual exclusionRLock: re-entrant lock for nested acquisition in same threadEvent: one-to-many signaling (“ready now”)Semaphore: bounded concurrent access (e.g., max 10 workers)Condition: wait/notify around state transitionsQueue(fromqueuemodule): thread-safe producer/consumer transport
Queue is often the safest default for work distribution.
Practical Pattern: Worker Queue
from queue import Queue
from threading import Thread
q = Queue()
def worker():
while True:
item = q.get()
if item is None:
q.task_done()
break
process(item)
q.task_done()
threads = [Thread(target=worker, daemon=True) for _ in range(4)]
for t in threads:
t.start()
for item in items:
q.put(item)
q.join()
for _ in threads:
q.put(None)
This model is common in crawlers and batch fetchers.
ThreadPoolExecutor
For many use cases, prefer concurrent.futures.ThreadPoolExecutor over manual thread management.
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=20) as pool:
futures = [pool.submit(fetch, url) for url in urls]
results = [f.result() for f in futures]
It handles worker lifecycle and task queueing cleanly.
Common Misconception
Misconception: adding more threads always makes Python faster.
Reality: too many threads can increase context switching, memory overhead, and lock contention. For CPU-heavy jobs, multiprocessing often scales better.
Choosing Between Async, Threads, and Processes
- async: high concurrency for I/O when libraries are async-native
- threads: easy integration with blocking I/O libraries
- processes: true parallelism for CPU-bound tasks
You often combine them in production systems.
Related Topics
This topic connects directly with Python Async/Await and Python Multiprocessing. Think of threads as a practical middle ground when full async rewrite is unnecessary but I/O concurrency is needed.
One Thing to Remember
In CPython, multithreading is mainly for I/O concurrency and responsiveness; keep shared state controlled with synchronization primitives, especially locks and queues.
See Also
- Python Async Await Async/await helps one Python program juggle many waiting jobs at once, like a chef who keeps multiple pots moving without standing still.
- Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
- Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
- Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
- Python Closures See how Python functions can remember private information, even after the outer function has already finished.