Skip to content

Threading & the GIL

Playground note: threading requires spawning OS threads — browser sandboxes cannot do this. Run the code snippets locally with python3. The <TsGo>, <Quiz>, and <ProgressTracker> components still work normally.

JavaScript gives you one model for concurrent I/O work: the single-threaded event loop with async/await. For CPU-heavy work you spawn a Worker, which runs in a completely separate thread with its own memory — there is no shared state by default.

Python’s threading module creates OS threads that share the same memory space. This makes passing data between threads easy, but it also means you have to protect shared data with locks to avoid race conditions.

TypeScript
// TypeScript — Web Worker (separate memory, message passing)
const worker = new Worker(new URL("./worker.ts", import.meta.url));
worker.postMessage({ numbers: [1, 2, 3, 4, 5] });
worker.onmessage = (e) => console.log("result:", e.data);
// Inside worker.ts:
self.onmessage = (e) => {
const sum = e.data.numbers.reduce((a: number, b: number) => a + b, 0);
self.postMessage(sum);
};
Python
# Python — threads share memory directly
import threading
results: list[int] = []
lock = threading.Lock()
def worker(numbers: list[int]) -> None:
total = sum(numbers)
with lock: # protect shared state
results.append(total)
t = threading.Thread(target=worker, args=([1, 2, 3, 4, 5],))
t.start()
t.join() # wait for thread to finish
print(results) # [15]
import threading
import time
def download(url: str) -> None:
print(f"Downloading {url}...")
time.sleep(1) # simulate I/O wait
print(f"Done: {url}")
urls = [
"https://api.example.com/users",
"https://api.example.com/posts",
"https://api.example.com/tags",
]
threads = [threading.Thread(target=download, args=(url,)) for url in urls]
for t in threads:
t.start() # start all threads
for t in threads:
t.join() # wait for all to finish
print("All downloads complete")

Run this locally — browser sandboxes can’t spawn OS threads/processes.

Threads do not return values directly (unlike async/await). The common pattern is to use a shared list or a queue.Queue:

import threading
import queue
def fetch_data(label: str, result_queue: queue.Queue) -> None:
# simulate work
import time; time.sleep(0.1)
result_queue.put(f"data from {label}")
q: queue.Queue[str] = queue.Queue()
threads = [
threading.Thread(target=fetch_data, args=(label, q))
for label in ["users", "posts", "tags"]
]
for t in threads:
t.start()
for t in threads:
t.join()
results = [q.get() for _ in threads]
print(results)
TypeScript
// TypeScript — you rarely think about this distinction because
// JS is always single-threaded. Workers are for any heavy work.
// asyncio/Promises handle all I/O natively.
Python
# Python — choose the right tool
# Use threading when:
# - Wrapping a blocking library that has no async version
# (e.g., an old database driver, a blocking SDK)
# - The task is I/O-bound (file reads, network calls, DB queries)
# - You need shared memory between tasks (vs multiprocessing)
# Prefer asyncio when:
# - You control the code and can use async libraries
# - You need thousands of concurrent I/O tasks
# (threads have OS overhead; coroutines are cheap)
# Use multiprocessing when:
# - The task is CPU-bound (math, image processing, ML inference)
import threading
counter = 0
lock = threading.Lock()
def safe_increment() -> None:
global counter
with lock: # acquire on enter, release on exit (even if exception)
counter += 1
threads = [threading.Thread(target=safe_increment) for _ in range(100)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # always 100 — lock prevents race condition
What does the GIL prevent in CPython?
For which type of workload do Python threads provide a genuine speedup?
How do Python threads differ from JavaScript Web Workers regarding memory?
What is the correct way to protect a shared variable accessed by multiple threads?