GIL & Concurrency

Difficulty: Advanced

Question

What is the GIL? How do you achieve parallelism in Python?

Answer

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)

Code examples

Threading vs Multiprocessing

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.

asyncio for I/O-Bound Concurrency

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.

When to Use What

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.

Key points

Concepts covered

GIL, Threading, Multiprocessing, asyncio, Concurrency vs Parallelism