Difficulty: Advanced
What is the GIL? How do you achieve parallelism in Python?
The GIL (Global Interpreter Lock) is a mutex in CPython that allows only one thread to execute Python bytecode at a time. This means Python threads cannot achieve true parallelism for CPU-bound tasks.
However: - I/O-bound tasks: threading works fine (GIL is released during I/O) - CPU-bound tasks: use multiprocessing (separate processes, each with its own GIL) - Async I/O: asyncio for thousands of concurrent I/O operations
Concurrency vs Parallelism: - Concurrency: dealing with many things at once (structure) - Parallelism: doing many things at once (execution)
import threading
import multiprocessing
import time
def cpu_bound(n):
"""CPU-intensive: count to n"""
total = 0
for i in range(n):
total += i
return total
# Single thread: ~4 seconds
start = time.time()
for _ in range(4):
cpu_bound(10_000_000)
print(f"Single: {time.time() - start:.2f}s")
# Threading: ALSO ~4 seconds (GIL!)
start = time.time()
threads = [threading.Thread(target=cpu_bound, args=(10_000_000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"Threading: {time.time() - start:.2f}s")
# Multiprocessing: ~1 second (true parallelism)
start = time.time()
with multiprocessing.Pool(4) as pool:
pool.map(cpu_bound, [10_000_000] * 4)
print(f"Multiprocessing: {time.time() - start:.2f}s")
Threading doesn't help CPU-bound tasks due to the GIL. Multiprocessing creates separate processes, each with its own Python interpreter and GIL.
import asyncio
import aiohttp
async def fetch_async(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_one(session, url) for url in urls]
return await asyncio.gather(*tasks)
async def fetch_one(session, url):
async with session.get(url) as response:
return response.status
# asyncio.gather runs all requests concurrently
urls = ['https://httpbin.org/delay/1'] * 10
results = asyncio.run(fetch_async(urls))
print(results)
asyncio uses a single thread with cooperative multitasking. While one request waits for I/O, others can proceed. Perfect for high-concurrency I/O.
import concurrent.futures
# CPU-bound -> multiprocessing
# I/O-bound (few connections) -> threading
# I/O-bound (many connections) -> asyncio
# Mix -> multiprocessing + asyncio per process
# ThreadPoolExecutor for I/O
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(fetch_url, url) for url in urls]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
# ProcessPoolExecutor for CPU
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(process_image, img) for img in images]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
concurrent.futures provides a unified interface for both threading and multiprocessing. The decision depends on whether your workload is CPU-bound or I/O-bound.
GIL, Threading, Multiprocessing, asyncio, Concurrency vs Parallelism