Creating Threads
Use threading.Thread to create and start threads.
import threading
import time
def download(site):
print(f"Downloading {site}...")
time.sleep(2) # Simulate network I/O
print(f"Done: {site}")
# Without threads: takes 6 seconds
# With threads: takes ~2 seconds
threads = []
for site in ["google.com", "github.com", "python.org"]:
t = threading.Thread(target=download, args=(site,))
threads.append(t)
t.start()
for t in threads:
t.join() # Wait for all to complete
print("All downloads done")The GIL – Global Interpreter Lock
The GIL prevents multiple threads from executing Python bytecode simultaneously. This means threads DO NOT speed up CPU-bound tasks (computation). They DO help I/O-bound tasks because threads release the GIL while waiting for I/O.
Use threading for I/O-bound tasks (HTTP, files, databases). Use multiprocessing for CPU-bound tasks (calculations, image processing). Using threads for heavy computation will NOT improve performance due to the GIL.
Thread Safety – Using Locks
When threads share data, use a Lock to prevent race conditions.
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock: # Only one thread at a time
counter += 1
threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Counter: {counter}") # Always 500000 with lockThreadPoolExecutor – High-Level API
concurrent.futures.ThreadPoolExecutor is the modern, higher-level threading API.
from concurrent.futures import ThreadPoolExecutor
import time
def fetch(url):
time.sleep(1) # Simulate I/O
return f"Data from {url}"
urls = ["api.com/1", "api.com/2", "api.com/3"]
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(fetch, urls))
for r in results:
print(r)The GIL: Why Python Threads Don't Speed Up Math
CPython has a Global Interpreter Lock — only one thread executes Python bytecode at a time. So threads give you concurrency (overlapping waits) but not parallelism (using multiple cores) for pure-Python computation.
| Task | Threads help? | Why |
|---|---|---|
| Download 100 URLs | ✅ yes | threads wait on I/O — GIL released during the wait |
| Sum a huge list of numbers | ❌ no | CPU-bound — GIL serializes it anyway |
from concurrent.futures import ThreadPoolExecutor
# I/O-bound → threads shine
with ThreadPoolExecutor() as pool:
pages = list(pool.map(download, urls))
from concurrent.futures import ProcessPoolExecutor
# CPU-bound → separate processes, each its OWN GIL/core
with ProcessPoolExecutor() as pool:
results = list(pool.map(heavy_math, chunks))
The rule of thumb: I/O-bound → threading or asyncio; CPU-bound → multiprocessing (dodges the GIL by running real separate processes). Also watch shared state: because threads interleave, unguarded shared variables cause race conditions — protect them with a threading.Lock.
🏋️ Practical Exercise
Work with threads:
- Create and start two
Threadobjects running the same function, thenjoin()them. - Show that a shared counter without a lock can produce wrong results.
- Fix it using a
threading.Lock. - Use
ThreadPoolExecutorto download (simulate) several URLs concurrently.
🔥 Challenge Exercise
Simulate downloading 10 web pages where each “download” sleeps for a second. Run them serially and time it, then run them with a ThreadPoolExecutor and show the dramatic speedup for this I/O-bound work. Add a shared results dictionary protected by a Lock to demonstrate thread-safe updates. Bonus: explain why the same approach would NOT speed up a CPU-bound version (the GIL).
📋 Summary
- Threads run within a single process and share memory.
- The Global Interpreter Lock (GIL) lets only one thread execute Python bytecode at a time.
- Because of the GIL, threading helps I/O-bound tasks but not CPU-bound ones.
- Shared mutable state can cause race conditions; protect it with a
threading.Lock. ThreadPoolExecutorfromconcurrent.futuresoffers a clean high-level API.- For CPU-bound parallelism, use multiprocessing instead.
Interview Questions on Multithreading
- What is a thread and what is multithreading?
- What is the GIL and how does it affect Python threads?
- When is threading beneficial in Python despite the GIL?
- What is a race condition and how do locks prevent it?
- What is the difference between
LockandRLock? - What does
ThreadPoolExecutorprovide? - What is the difference between threading and multiprocessing?
Related Topics
FAQ
The Global Interpreter Lock is a mutex in CPython that allows only one thread to run Python bytecode at a time. It simplifies memory management but prevents threads from running CPU-bound Python code in true parallel.
No. The GIL is released during blocking I/O (network, disk), so threads still overlap waiting and speed up I/O-bound work significantly. It only limits CPU-bound parallelism, where multiprocessing is the answer.
It is a bug where the result depends on the unpredictable timing of threads accessing shared data — for example, two threads incrementing the same counter and losing updates. A Lock serializes access to prevent it.
Threads share memory within one process and are limited by the GIL; great for I/O. Processes have separate memory and their own GIL, giving true CPU parallelism at the cost of higher overhead.

