---
title: 'The Global Interpreter Lock'
source: 'https://academia.sh/en/courses/python-concurrency/global-interpreter-lock'
course: 'Concurrency and Performance'
language: en
updated: '2026-08-17T18:10:24+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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** — `trace` is `run`'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

```python
"""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.
