Lesson 01 / 10
The Global Interpreter Lock
This course counts overlapping steps, not duration; in an eighty-step setup, a single thread gives 80 ticks and 0 overlapping steps on every load, a single-slot regime takes at most 1 CPU step per tick, and the I/O-bound load's 26 CPU steps spread across 26 separate ticks.
Contents
The previous course spent eleven lessons measuring a single question: who answered the call, who enforced the contract. Its close exposed an assumption those measurements never wrote down — single thread. When asked which class answered, a single resolution chain ran; when asked which layer caught the violation, calls ran one finishing before the next started. Under a single thread, “who answered” had exactly one answer every time.
With more than one thread, the question changes shape. What is asked now is not who answered, but who could advance at the same time. This course counts that question over ten lessons, and starts the first at its narrowest point: how many threads can take a CPU step at once in Python’s interpreter.
The Lock Is a Number
The global interpreter lock (GIL) is a restriction the interpreter places to keep its own internal structures consistent: only one thread executes interpreter bytecode at a time. A thread not holding the lock waits. The lock is given to one thread at a time, released, and passed to another.
In this lesson’s view, the lock is not a mechanism, it is a number: how many threads can take a CPU step at once. The course’s measure name for this number is slot. With the lock in place, the slot count is one.
Places where the lock gets released do not disturb this number, because a step where the lock is released is not a CPU step to begin with. A thread reading a file, waiting on a network reply, or sleeping does not execute interpreter bytecode; the lock passes to another thread meanwhile. This is the course’s second step type: the I/O step. Slot count does not bound these.
Process, thread, scheduling, and context switching were built as mechanisms in the Operating System Concepts course and are not repeated here. The question here is not how the operating system orders threads, it is which step Python can overlap.
Why We Do Not Measure Duration
The first instinct for a concurrency measurement is comparing duration. This course does not do that, and will not in any lesson.
Real duration depends on three things at once: the machine itself, its load at the moment, and when the measurement started. All three are outside the writing. The same code run twice on the same machine gives two different numbers, and cannot be compared across two different machines. A claim written with duration cannot be reproduced where it is read.
What is needed instead is something countable: if more than one task advances in a tick, one less than the number that advanced counts as overlapped. That is the course’s measure, called the overlapping step. It comes from the setup, is read from the run, and gives the same number everywhere.
The ban is not limited to seconds. A speedup factor is a duration claim too and carries the same problem; “N times faster” says nothing unless it also states which work was measured under which condition. A lesson near the course’s end measures this from the other direction: same work, same regime, the only difference being what the measurement counts — and two different numbers come out. This lesson lays the ground for that claim; the counted unit gets named in every measurement.
The Setup
Eight tasks, ten steps each; 80 steps total. Every step is either a CPU step or an I/O step. A load is defined by its share of I/O steps: seven in ten for an I/O-bound load, five in ten for balanced, one in ten for CPU-bound.
Tasks advance tick by tick in a deterministic scheduler. Two numbers decide how many tasks can advance in one tick: worker, the number of tasks that can advance at once; and CPU slot, how many tasks can take a CPU step in one tick. A single slot means CPU steps get taken one at a time.
The measurement’s assumptions:
- CM1 — The oracle is the setup itself: we generated the eight tasks’ step sequences, so which step is which type is known.
- CM2 — Eight tasks, ten steps per task, eighty steps total. The three loads differ only in I/O share; step count is the same in all three.
- CM3 — The tick is the scheduler’s unit. In one tick, a task takes at most one step.
- CM4 — Worker is the most tasks that can advance in one tick; order is given by task index.
- CM5 — CPU slot is how many tasks can take a CPU step in one tick. A single slot is this model’s counterpart to the global interpreter lock.
- CM6 — An I/O step is unaffected by slots; this is the step where the lock gets released.
- CM7 — Overlapping steps are one less than the number of tasks that advance in a tick; zero if no task advances.
- CM8 — The single-thread regime has one worker, one slot: one task, one step per tick.
- CM9 — Duration is never measured or written. What is counted is steps, ticks, and overlapping steps.
- CM10 —
traceisrun‘s report twin: it makes the same decisions in the same order and does not change behavior. The output’s last line shows the two give the same number. - CM11 — The measurement is a single run; the seed is fixed and the setup is deterministic.
- CM12 — This lesson looks only inside the tick. Regimes’ total tick counts are the next lessons’ measure and are not printed here.
The Measurement
"""Global interpreter lock: how many tasks can advance in one tick.""" SEED = 20260817 CPU, IO = "cpu", "io" def make_rng(seed): state = seed % 2147483646 + 1 def draw(n): nonlocal state state = (state * 48271) % 2147483647 return state % n return draw def tasks(count=8, steps=10, io_share=7, seed=SEED): """Each task is a step sequence; io_share/10 fraction are I/O steps.""" draw, result = make_rng(seed), [] for i in range(count): result.append([IO if draw(10) < io_share else CPU for _ in range(steps)]) return result def run(jobs, workers, cpu_slots): """Deterministic scheduler advancing tick by tick. workers : how many tasks can advance at the same time cpu_slots : how many tasks can take a CPU step in one tick Returns: tick count, overlapping steps, cpu steps, io steps """ remaining = [list(j) for j in jobs] tick = overlap = cpu_steps = io_steps = 0 while any(remaining): active = [i for i, j in enumerate(remaining) if j][:workers] if not active: break slots_left, advanced = cpu_slots, 0 for i in active: step = remaining[i][0] if step == CPU: if slots_left <= 0: continue slots_left -= 1 cpu_steps += 1 else: io_steps += 1 remaining[i].pop(0) advanced += 1 tick += 1 overlap += max(0, advanced - 1) return tick, overlap, cpu_steps, io_steps def trace(jobs, workers, cpu_slots): """run's report twin: dumps the steps taken each tick, turn by turn.""" remaining, rows = [list(j) for j in jobs], [] while any(remaining): active = [i for i, j in enumerate(remaining) if j][:workers] if not active: break slots_left, cpu, io = cpu_slots, 0, 0 for i in active: if remaining[i][0] == CPU: if slots_left <= 0: continue slots_left -= 1 cpu += 1 else: io += 1 remaining[i].pop(0) rows.append((cpu, io)) return rows LOADS = (("I/O bound", 7), ("balanced", 5), ("CPU bound", 1)) print(f"{'load':<20s} {'steps':>5s} {'cpu':>5s} {'io':>5s} " f"{'single-thread ticks':>20s} {'overlap':>8s}") for label, share in LOADS: j = tasks(io_share=share) tick, overlap, cpu_steps, io_steps = run(j, 1, 1) print(f"{label:<20s} {sum(len(t) for t in j):5d} {cpu_steps:5d} " f"{io_steps:5d} {tick:20d} {overlap:8d}") j = tasks(io_share=7) rows = trace(j, 8, 1) print() print("single slot, eight workers, I/O bound load — first twelve ticks") print(f"{'tick':>4s} {'advanced':>9s} {'cpu':>5s} {'io':>5s} {'overlap':>8s}") for i, (cpu, io) in enumerate(rows[:12], 1): print(f"{i:4d} {cpu + io:9d} {cpu:5d} {io:5d} {max(0, cpu + io - 1):8d}") print(f"steps taken in first twelve ticks {sum(cpu + io for cpu, io in rows[:12])}, " f"overlapping steps {sum(max(0, cpu + io - 1) for cpu, io in rows[:12])}") print() print(f"most cpu steps taken in one tick {max(cpu for cpu, _ in rows)}") print(f"ticks carrying a cpu step {sum(1 for cpu, _ in rows if cpu)}, " f"total cpu steps {sum(cpu for cpu, _ in rows)}") print(f"trace and run agree: cpu " f"{sum(cpu for cpu, _ in rows) == run(j, 8, 1)[2]}, io " f"{sum(io for _, io in rows) == run(j, 8, 1)[3]}")
load steps cpu io single-thread ticks overlap I/O bound 80 26 54 80 0 balanced 80 40 40 80 0 CPU bound 80 73 7 80 0 single slot, eight workers, I/O bound load — first twelve ticks tick advanced cpu io overlap 1 5 1 4 4 2 5 1 4 4 3 5 1 4 4 4 5 1 4 4 5 3 1 2 2 6 4 1 3 3 7 4 1 3 3 8 5 1 4 4 9 5 1 4 4 10 3 1 2 2 11 2 1 1 1 12 1 1 0 0 steps taken in first twelve ticks 47, overlapping steps 35 most cpu steps taken in one tick 1 ticks carrying a cpu step 26, total cpu steps 26 trace and run agree: cpu True, io True
Single Thread: Load Composition Changes Nothing
The upper table’s last two columns are the same in all three rows: 80 ticks, 0 overlapping steps.
This is not a performance result, it is a definition. In the single-thread regime, worker is one; one task advances per tick, and one advancing task minus one is zero. Eighty steps make eighty ticks. Load composition has no effect: in the I/O-bound load, 54 steps could have released the lock, but with no other thread to release it to, releasing it pays off nothing.
This gives the course’s first reading: overlap is a property of the regime, not the load. The same eighty steps overlap on no load if there is no other task to advance.
The third and fourth columns, though, are the load’s own property, independent of regime. The I/O-bound load has 26 CPU, 54 I/O steps; the CPU-bound load has 73 CPU, 7 I/O steps. The total is 80 in all three. These two numbers stay fixed throughout the measurement, because the regime does not change a step’s type, only which tick it gets taken in.
Inside the Tick
The lower table opens up the single-slot regime’s first twelve ticks row by row, and this is the lesson’s real measurement.
The CPU column is 1 in all twelve rows. The most CPU steps taken in one tick is 1, and this holds for the entire run. Whatever the slot count is, that is the upper bound on CPU steps per tick; here the slot is one, so the bound is one.
The I/O column ranges between 0 and 4. Four tasks can take an I/O step in the same tick, because those steps do not use the slot. In the first tick, five tasks advance: four I/O steps, one CPU step. Four overlapping steps are earned in that tick.
With eight tasks alive, only five advancing might look odd at first. The reason is in the columns: at that tick, four tasks’ next step is a CPU step, and there is only one slot. Whichever task takes the slot advances, the other three wait. A waiting task takes no step at all that tick.
The twelfth row shows the lock at its plainest: 1 advanced, 1 CPU step, 0 overlapping steps. Only one task could advance at that tick, and its step was a CPU step. A tick like this is indistinguishable from the single-thread regime.
Worker and Slot Are Two Separate Numbers
The model has two settings, and confusing them is this topic’s most common mistake.
Worker says how many tasks can enter the queue in a tick. Entering the queue does not guarantee advancing: at the twelfth tick, two tasks were alive, one entered and advanced, the other’s step was a CPU step and the slot was taken. Slot says how many of those in the queue can take a CPU step.
Since the two are set independently, the model produces four separate regimes, and each of this course’s remaining lessons changes one of these two numbers. Opening a thread grows the worker count; since the interpreter is single, it does not grow the slot. Starting a separate process, though, means a separate interpreter, and grows the slot. Asynchronous programming takes a third path: it asks the operating system for neither worker nor slot, and leaves the queueing decision to the code itself.
This distinction matters because “concurrency” is not one thing, it is a set of choices. Adding a thread to a program does not speed it up under every load; what gets added is a worker, and the only thing a worker can earn is overlap in I/O steps. In the table above, the CPU-bound load has 7 steps that could overlap; giving it eight workers cannot push the gain past that number. How many workers earn how much is the next two lessons’ measure, but the upper bound is already written in this lesson: overlapping steps can never exceed the number of steps that could overlap.
The Step That Cannot Overlap
The last two lines give this lesson’s structural finding: ticks carrying a CPU step 26, total CPU steps 26.
The two numbers being equal is not a coincidence, it is the direct result of
a single slot. Since a tick can carry at most one CPU step, 26 CPU steps
have to spread across at least 26 separate ticks. The general rule: in
a single-slot regime, tick count can never drop below CPU step count.
With slot count y, the lower bound is CPU step count divided by y,
rounded up.
This is where the course’s measurement axis comes from. Not all 80 steps of a load are equal: in the I/O-bound load, 54 steps can overlap, 26 cannot. What sets the upper bound on the gain expected from concurrency is not task count or worker count, it is the share of steps that can overlap.
Of the 47 steps taken in the first twelve ticks, 35 are overlapping. The difference between these two numbers is exactly 12 — the tick count. This is the identity that holds throughout the model: tick count is found by subtracting overlapping steps from total steps. Counting overlapping steps and counting ticks are two faces of the same measurement; this is why the course never needs duration.
Summary
- In this course, the global interpreter lock is not a mechanism, it is a number: how many tasks can take a CPU step in one tick. With the lock in place, this number — the slot — is one.
- I/O steps do not use the slot; these are the steps where the lock gets released, and in a single-slot regime, four tasks can take one in the same tick.
- The single-thread regime gives 80 ticks and 0 overlapping steps on all three loads. Overlap is a property of the regime, not the load; no load overlaps without a second task to advance.
- With a single slot, the most CPU steps taken in one tick is 1; the I/O-bound load’s 26 CPU steps spread across 26 separate ticks, and tick count cannot drop below that number.
- Tick count is found by subtracting overlapping steps from total steps; this is why overlapping steps are enough to compare without measuring duration.
Next Step
This lesson looked inside the tick and never wrote the total tick count. The next lesson counts it: running eight workers with a single slot — the thread regime — finishes eighty steps in how many ticks? The answer is not one number, it is two — because the same mechanism behaves completely differently on an I/O-bound load versus a CPU-bound one, and where that difference comes from is already written in this lesson’s last two lines.
To keep your progress and take notes, Log in
My notes
Log in to take notes.