Vasu Patel logoVasu Patel
← Sharing Time: Concurrency & Multithreading
All chapters in this track
  1. 01Chapter 1: Concurrency vs. Parallelism, and Why It's Hard
  2. 02Chapter 2: Processes vs. Threads, Revisited for Concurrency
  3. 03Chapter 3: The Core Hazards: Race Conditions, Deadlock, Livelock, Starvation
  4. 04Chapter 4: Synchronization Primitives: Locks, Semaphores, Monitors, Condition Variables
  5. 05Chapter 5: Designing for Thread Safety: Immutability, Confinement, Thread-Safe Collections
  6. 06Chapter 6: Classic Patterns: Producer-Consumer, Reader-Writer, Thread Pools
  7. 07Chapter 7: Higher-Level Models: Async/Await, Event Loops, Coroutines, Actors, Channels
  8. 08Chapter 8: Memory Models and Visibility: Happens-Before, Volatile, Atomics
  9. 09Chapter 9: Lock-Free and Wait-Free Programming Basics
  10. 10Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop
  11. 11Chapter 11: Debugging and Testing Concurrent Code
  12. 12Chapter 12: Classic Interview Problem: Producer-Consumer
  13. 13Chapter 13: Classic Interview Problem: Dining Philosophers
  14. 14Chapter 14: Classic Interview Problem: Readers-Writers
  15. 15Chapter 15: Concurrency in System Design and Production Debugging

Chapter 10 of 15

Chapter 10: How Concurrency Differs Across Runtimes: JVM Threads, Python's GIL, Go's Goroutines, JavaScript's Event Loop

Why this matters

Every concept in this book so far is conceptually universal, but its practical consequences differ dramatically depending on which runtime you're actually working in — and nowhere is this more consequential, or more commonly misunderstood, than Python's Global Interpreter Lock. This chapter explains the GIL precisely, then surveys three other major runtimes' fundamentally different approaches, so you can reason correctly no matter which one you're working in.

Python's GIL, explained precisely

[SENIOR INSIGHT] CPython (the standard, most widely-used Python implementation) has a Global Interpreter Lock (GIL) — a single, process-wide lock that only one thread can hold at a time, and a thread must hold the GIL to execute any Python bytecode at all. This means true parallel execution of Python code across multiple threads, in CPython, is not possible — no matter how many threads you create, only one Python bytecode instruction executes at any literal instant.

import threading
import time

def cpu_bound_work():
    total = 0
    for i in range(50_000_000):
        total += i
    return total

# Single-threaded:
start = time.time()
cpu_bound_work()
print(f"Single-threaded: {time.time() - start:.2f}s")

# "Parallel" with 2 threads -- but NOT actually faster, because of the GIL:
start = time.time()
t1 = threading.Thread(target=cpu_bound_work)
t2 = threading.Thread(target=cpu_bound_work)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Two threads: {time.time() - start:.2f}s")
# The two-thread version does roughly the SAME total wall-clock time as running
# the work twice sequentially would -- NOT half, because the GIL serializes
# actual bytecode execution regardless of thread count.

Why the GIL exists: it dramatically simplifies CPython's internal memory management (reference counting for garbage collection, specifically, becomes trivially thread-safe if only one thread ever touches Python objects at a time) — a genuine engineering trade-off made early in CPython's history, not an oversight.

What the GIL does NOT prevent: I/O-bound concurrency — a thread waiting on network or disk I/O releases the GIL during that wait (since it's not executing Python bytecode during a blocking syscall), letting other threads run — which is exactly why threading remains genuinely useful in Python for I/O-bound work (the exact case Chapter 6's ThreadPoolExecutor example used) even though it provides zero benefit for CPU-bound work.

CPU-bound work + threading: NO speedup (GIL serializes bytecode execution)
I/O-bound work + threading: REAL speedup (GIL released during I/O waits)
CPU-bound work + multiprocessing: REAL speedup (separate processes, separate GILs)

[IN PRACTICE] This single fact — the GIL applies to bytecode execution, not to waiting — is the most commonly misunderstood aspect of Python concurrency, and getting it right explains exactly when threading helps (I/O-bound) versus when multiprocessing (Chapter 2) is required instead (CPU-bound).

The JVM: true thread-level parallelism, native memory model

[BEGINNER] The Java Virtual Machine has no GIL-equivalent — Java threads can execute genuinely in parallel across multiple CPU cores, which is exactly why Java's synchronization primitives (synchronized, java.util.concurrent's locks, atomics) and its formally-specified memory model (Chapter 8's happens-before relationships are drawn directly from the Java Memory Model specification) matter so directly and immediately — every hazard in Chapter 3, and every visibility concern in Chapter 8, is a first-order, constant concern in Java in a way it simply isn't in single-GIL CPython.

Go's goroutines and channels

[BEGINNER] Go takes a different approach: goroutines are extremely lightweight, runtime-managed logical threads (far cheaper to create than OS threads — a program can run hundreds of thousands of them), multiplexed onto a smaller number of actual OS threads by Go's own runtime scheduler, and Go's standard idiom strongly favors channels (Chapter 7's CSP model) over shared-memory locking for communication between goroutines — Go's own documentation states this philosophy explicitly: "don't communicate by sharing memory; share memory by communicating." Go achieves genuine parallelism (goroutines can run on multiple actual cores simultaneously, no GIL-equivalent restriction), while encouraging a programming style that sidesteps many of Chapter 3's hazards by design, through the channel-based architectural discipline from Chapter 7.

JavaScript's single-threaded event loop

[BEGINNER] JavaScript (in browsers and in Node.js) runs application code on a single thread, with an event loop conceptually similar to Python's asyncio (Chapter 7) — I/O operations are non-blocking, and callbacks/promises resume execution at specific points, never preemptively. This means JavaScript code between any two suspension points (an await, the end of a synchronous callback) never races with other JavaScript code, since there is fundamentally only one thread ever executing JavaScript at all — a genuinely different, and in some ways simpler, foundational guarantee than any of the other three runtimes discussed in this chapter. (Modern JavaScript does support genuine multi-threading via Web Workers/worker threads, but each worker has its own fully isolated memory — communication happens via explicit message passing, directly the actor-model-like discipline from Chapter 7, not shared memory.)

A comparison table

RuntimeTrue parallel execution of its own code?Primary concurrency hazard exposure
CPythonNo, for CPU-bound work (GIL); yes, for I/O-bound waitingChapter 3's hazards apply fully to I/O-bound multi-threaded code; CPU-bound work needs multiprocessing instead
JVM (Java)Yes, fullyChapter 3's hazards and Chapter 8's visibility concerns are constant, first-order concerns
GoYes, fully (via goroutines on OS threads)Mitigated by strong channel/CSP idioms (Chapter 7), though shared-memory hazards remain possible if channels are bypassed
JavaScript (main thread)No — single-threaded by designNo same-thread races possible; Web Workers reintroduce true parallelism via isolated, message-passing units

Common mistakes

  1. Assuming Python's threading module behaves like Java's threads, expecting a CPU-bound speedup that the GIL specifically prevents — this is the single most common cross-language concurrency misconception.
  2. Assuming the GIL means Python threads are useless for concurrency generally — it remains genuinely valuable for I/O-bound work specifically, since the GIL is released during I/O waits.
  3. Writing Go code with heavy shared-memory locking out of habit (from a background in Java or C++) instead of leaning into Go's idiomatic channel-based communication style, missing much of the language's designed-in safety benefit.
  4. Not realizing JavaScript's Web Workers use isolated memory with message passing, and expecting shared-memory-style direct access between a worker and the main thread the way threads within one process work in other languages.

Discussion / interview questions

  1. "What is Python's GIL, and why doesn't multi-threading speed up CPU-bound Python code?" Strong answer: The GIL is a single, process-wide lock that only one thread can hold at a time, required to execute any Python bytecode — this serializes bytecode execution regardless of thread count, so CPU-bound work sees no speedup from more threads; multiprocessing (separate processes, separate GILs) is needed for genuine CPU-bound parallelism instead.

  2. "Why does Python's threading module still provide real value for I/O-bound work, despite the GIL?" Strong answer: A thread releases the GIL while blocked on I/O (since it's not executing Python bytecode during a blocking syscall), allowing other threads to run during that wait — this is exactly the concurrency (not parallelism, per Chapter 1) benefit that helps I/O-bound workloads.

  3. "How does Go's approach to concurrency differ philosophically from Java's?" Strong answer: Go strongly favors channel-based communication between lightweight, runtime-managed goroutines ("share memory by communicating") over Java's more traditional shared-memory-plus-locks model, while both achieve genuine parallel execution across cores — Go's idiomatic style sidesteps many classic hazards by architectural discipline rather than eliminating the underlying possibility of them entirely.

  4. "Why can't two pieces of JavaScript code running on the main thread ever have a classic race condition with each other?" Strong answer: JavaScript's main thread executes only one piece of code at a time, with switching happening only at explicit suspension points (via the event loop) rather than preemptively — there's fundamentally only one thread, so there's no possibility of two pieces of main-thread code executing simultaneously and racing over shared state, though logical races across await/callback boundaries (Chapter 7's async warning) are still possible.

Chapter summary / cheat sheet

  • CPython's GIL: serializes bytecode execution — no CPU-bound speedup from threading, but real I/O-bound concurrency benefit since the GIL releases during I/O waits; multiprocessing is needed for genuine CPU-bound parallelism.
  • JVM (Java): true thread-level parallelism, no GIL-equivalent — Chapter 3's hazards and Chapter 8's visibility concerns are constant, first-order concerns requiring explicit synchronization.
  • Go: lightweight goroutines with true parallelism, strongly idiomatic channel-based ("share memory by communicating") communication style.
  • JavaScript: single-threaded main-thread execution with an event loop (no same-thread races possible); Web Workers add true parallelism via fully isolated memory and message passing.
  • The practical lesson: know your specific runtime's actual concurrency model before assuming a technique or assumption from one language transfers directly to another — the GIL misconception is the single most common cross-language error in this space.