Threading & the GIL
Playground note:
threadingrequires spawning OS threads — browser sandboxes cannot do this. Run the code snippets locally withpython3. The<TsGo>,<Quiz>, and<ProgressTracker>components still work normally.
Threads vs the event loop
Section titled “Threads vs the event loop”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 — 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 — threads share memory directlyimport 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 finishprint(results) # [15]Basic threading
Section titled “Basic threading”import threadingimport 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.
Thread with return value
Section titled “Thread with return value”Threads do not return values directly (unlike async/await). The common pattern is to use a shared list or a queue.Queue:
import threadingimport 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)When to use threading
Section titled “When to use threading”// 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 — 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)Thread safety and locks
Section titled “Thread safety and locks”import threading
counter = 0lock = 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