Lesson 02 / 10
Threads
Eight workers and a single slot finish eighty steps in 31 ticks on the I/O-bound load (gain 49, ratio 0.6125), and only 73 ticks on the CPU-bound load (gain 7); same mechanism, a sevenfold difference, and the gain is bounded in every load by the number of steps that can overlap.
Contents
The previous lesson looked inside the tick and never wrote the total tick count. In the single-slot regime’s first twelve ticks, the CPU column was always 1, while the I/O column climbed as high as four. From that came a lower bound: 26 CPU steps have to spread across at least 26 separate ticks.
This lesson asks for the number itself. Queueing eight tasks at once — opening a thread — finishes eighty steps in how many ticks? The answer is not one number. The same mechanism gives two entirely different numbers depending on load composition, and what this lesson measures is the size of that difference.
A Thread Adds a Worker, Not a Slot
A thread is a separate execution flow within the same process. Process, thread, scheduling, and context switching were built as mechanisms in the Operating System Concepts course and are not repeated here. This lesson’s question is not mechanism, it is a number: which setting of the model does opening a thread change?
The answer is written in the previous lesson’s two settings. Eight threads means eight tasks can enter the queue at once; that is, worker count rises to eight. But all eight threads are still inside the same interpreter, and that interpreter’s lock is single. The slot stays at one.
The direct consequence: adding a thread lets I/O steps overlap; it does not let CPU steps overlap. A thread releases the lock while waiting on a network reply, and another thread advances. If both want to execute bytecode at the same instant, one waits.
This distinction corrects both of two common claims about threads. “A thread makes work parallel” is true only for I/O steps; CPU steps have no parallelism, only order. “Threads are useless because of the lock” misses the I/O steps. Both share the same gap: not saying which step is meant. The measurement below counts exactly this distinction.
The Regime Is Modeled, Not Run
This lesson does not start real threads, and that is not a convenience, it is the measurement’s condition. A program that actually opens eight threads leaves which thread advances at which moment to the operating system’s scheduler. The same program run twice produces two different orders; the written output will not reproduce on the reader’s machine. What is meant to be measured — which step overlaps — stays buried under that noise.
Instead, scheduling gets modeled: the ordering rule is written down, the seed is fixed, and the run gives the same number everywhere.
The measurement’s assumptions:
- CM13 — The core is the same as the previous lesson: eight tasks, ten steps per task, eighty steps total; the seed is fixed and the setup is deterministic.
- CM14 — The thread regime’s counterpart in the model is worker 8, slot 1.
- CM15 — No real thread is started; if one were, the output would depend on the scheduler and would not reproduce.
- CM16 — The comparison baseline is the single-thread regime (worker 1, slot 1), and it gives 80 ticks on all three loads.
- CM17 — Gain is the regime’s tick count subtracted from the single-thread tick count; ratio is gain divided by the single-thread tick count. This is not a speedup factor, it is the difference between two tick counts.
- CM18 — The lower bound is the rule built in the previous lesson: in a single slot, tick count cannot drop below CPU step count.
- CM19 — The worker sweep uses the same task sequence; only worker count changes.
- CM20 — Context-switch cost is zero in the model. A real system has this cost, and it is outside the model; what is measured is not the size of the cost, it is the bound on overlap.
- CM21 — Tasks are independent of each other; shared state, locking, and races are outside this measurement.
- CM22 — Duration is never measured. The counted unit is steps, ticks, and overlapping steps.
- CM23 — The measurement is a single run; all three loads are generated from the same core.
The Measurement
"""Threads: same mechanism, two loads, two separate gains.""" 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): """Returns: tick, 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 LOADS = (("I/O bound", 7), ("balanced", 5), ("CPU bound", 1)) print("thread regime: eight workers, single slot") print(f"{'load':<22s} {'steps':>5s} {'single-thread':>13s} {'thread':>9s} " f"{'overlap':>8s} {'gain':>7s} {'ratio':>7s}") for label, share in LOADS: j = tasks(io_share=share) single = run(j, 1, 1)[0] tick, overlap, _, _ = run(j, 8, 1) print(f"{label:<22s} {sum(len(t) for t in j):5d} {single:13d} {tick:9d} " f"{overlap:8d} {single - tick:7d} {(single - tick) / single:7.4f}") print() print("lower bound versus reached tick") print(f"{'load':<22s} {'cpu steps':>9s} {'lower bound':>11s} {'reached':>9s} " f"{'diff':>5s}") for label, share in LOADS: j = tasks(io_share=share) tick, _, cpu_steps, _ = run(j, 8, 1) print(f"{label:<22s} {cpu_steps:9d} {cpu_steps:11d} {tick:9d} " f"{tick - cpu_steps:5d}") print() print("as worker count grows (slot stays one)") print(f"{'worker':>6s} " + " ".join(f"{label:>13s}" for label, _ in LOADS)) print(f"{'':6s} " + " ".join(f"{'tick / overlap':>13s}" for _ in LOADS)) for workers in (1, 2, 4, 8): cells = [] for label, share in LOADS: tick, overlap, _, _ = run(tasks(io_share=share), workers, 1) cells.append(f"{f'{tick} / {overlap}':>13s}") print(f"{workers:6d} " + " ".join(cells)) print() j_io, j_cpu = tasks(io_share=7), tasks(io_share=1) g_io = run(j_io, 1, 1)[0] - run(j_io, 8, 1)[0] g_cpu = run(j_cpu, 1, 1)[0] - run(j_cpu, 8, 1)[0] print(f"same mechanism, two loads: gain {g_io} ticks versus {g_cpu} ticks, " f"ratio {g_io / g_cpu:.1f}") print(f"CPU-bound load io steps {run(j_cpu, 8, 1)[3]}, " f"gain {g_cpu}; I/O-bound load io steps " f"{run(j_io, 8, 1)[3]}, gain {g_io}")
thread regime: eight workers, single slot
load steps single-thread thread overlap gain ratio
I/O bound 80 80 31 49 49 0.6125
balanced 80 80 42 38 38 0.4750
CPU bound 80 80 73 7 7 0.0875
lower bound versus reached tick
load cpu steps lower bound reached diff
I/O bound 26 26 31 5
balanced 40 40 42 2
CPU bound 73 73 73 0
as worker count grows (slot stays one)
worker I/O bound balanced CPU bound
tick / overlap tick / overlap tick / overlap
1 80 / 0 80 / 0 80 / 0
2 44 / 36 52 / 28 73 / 7
4 34 / 46 44 / 36 73 / 7
8 31 / 49 42 / 38 73 / 7
same mechanism, two loads: gain 49 ticks versus 7 ticks, ratio 7.0
CPU-bound load io steps 7, gain 7; I/O-bound load io steps 54, gain 49
Same Mechanism, a Sevenfold Difference
The upper table’s two ends carry this lesson’s entire claim.
On the I/O-bound load, eight workers and a single slot finish eighty steps in 31 ticks. Gain 49 ticks, ratio 0.6125. Overlapping steps are also 49: the identity that tick count is found by subtracting overlapping steps from total steps shows up here exactly.
On the CPU-bound load, the same mechanism finishes eighty steps in only 73 ticks. Gain 7 ticks, ratio 0.0875.
The only thing that changes is step type. Task count is the same, step count is the same, worker count is the same, slot is the same, scheduler is the same. The last line’s ratio is 7.0 — the difference between the gains is exactly sevenfold.
The balanced load sitting in between shows both ends lie on the same curve: 80 ticks drops to 42, gain 38, ratio 0.4750. As I/O share falls from seven in ten to five in ten, gain drops from 49 to 38; at one in ten, to 7. Gain behaves like a step function of I/O share, and all three measurement points point the same direction.
The reading is this: concurrency is not a speedup mechanism. The same regime overlaps more than half the work on one load, and cannot get close to a tenth on the other. When a thread is added to a program and no gain shows up, what is missing may not be thread count, but overlappable steps in that program.
The Gain’s Upper Bound Is Written in the Code
The last line also says where the gain comes from, placing two numbers side by side.
On the CPU-bound load, I/O steps are 7, gain is 7. The two numbers are equal. On the I/O-bound load, I/O steps are 54, gain is 49 — gain sits a bit below the number of overlappable steps, but never above it.
The rule: gain can never exceed the number of overlappable steps. Since CPU steps have to be taken one at a time in a single-slot regime, the only thing that can be shaved off tick count is I/O steps. On the CPU-bound load, only seven steps could overlap; not eight, and certainly not eighty.
The middle table shows the same bound from the other end. The lower bound was the rule built in the previous lesson: in a single slot, tick count cannot drop below CPU step count. On the I/O-bound load, lower bound is 26, reached is 31; diff 5. On the CPU-bound load, lower bound is 73, reached is 73; diff 0.
The last row matters: on the CPU-bound load, the regime sits exactly on the lower bound. Nothing better is possible, because each of the 73 CPU steps demands its own tick. Anyone looking for improvement on this load gets nothing by playing with thread count; what needs to change is slot count, and that is not a number a thread can change.
The 5-tick gap on the I/O-bound load is meaningful too: 26 of the 31 ticks carry a CPU step, the remaining 5 carry only I/O steps. In those five ticks, the slot sits empty, because no live task’s next step is a CPU step at that moment. The empty slot looks like a loss, but it is not fixable: which task wants a CPU step at which tick is written into the step sequence, not something the scheduler can choose.
Where Adding Workers Stops
The lower table raises worker count from 1 to 8, giving three different behaviors on the three loads.
On the CPU-bound load, the second worker drops 80 ticks to 73; the fourth and eighth workers add nothing. Tick count freezes at 73, overlapping steps at 7. Seven steps could overlap, and all seven overlapped with the second worker; nothing was left for a third. Raising thread count has no countable payoff on this load.
On the I/O-bound load, the curve is not flat but it is declining: the second worker earns 36 ticks, the third and fourth together earn 10, the fifth through eighth together earn 3. Gain does not grow linearly with worker count; the first few workers already claim most of the overlappable steps.
On the balanced load, the same shape flattens sooner: from 80 to 52, then 44, then 42. Going to eight workers over four only earns two more ticks.
The table’s first row carries information too: with a single worker, all three loads give 80 ticks / 0 overlapping steps. Running eight tasks with one worker is single-threaded no matter what the regime is called. When a program’s thread pool width is dropped to one, what is measured is not concurrency, only order. A comparison that does not write down pool width is unreadable for this reason.
The common lesson across the three columns: adding workers earns until overlappable steps run out, then stops. What decides where it stops is not worker count, it is load composition. And nowhere in this table does the CPU-bound column drop below 73 — there is no way past that bound in this lesson’s regime.
What the Model Leaves Out
A measurement is unreadable unless it states what it counts, so two things this model does not count need naming.
Context-switch cost counts as zero. In a real system, switching from one thread to another is not free; there is state to save. The model never counts this cost, so the gain numbers above are written on the optimistic side. This does not invalidate the results: adding the cost only shrinks the gain, it does not flip its sign. The CPU-bound load’s 7 ticks were already small; adding the cost shrinks them further, and reinforces that threads are pointless on that load.
Shared state is ignored. The eight tasks are independent of each other; none reads what another writes. In a real program, writing to a shared dictionary from two threads requires a lock in between, and waiting on that lock adds a new kind of wait. This falls outside what the model measures, but its direction is clear: waiting on a lock reduces overlapping steps, because a waiting task does not advance in that tick.
Both together mean this: the measured 49 and 7 are the upper bound on the gain that can be expected from threads. A real program can only approach these numbers. If a gain claim sits above this bound, something is uncounted in the measurement.
A separate warning follows from this: a thread is a mechanism for waiting steps. If a program is computing instead of waiting, it does not fail to gain because of the lock — it fails because there is nothing to overlap.
Summary
- Opening a thread grows worker count in the model, not slot count; all eight threads still share the same interpreter’s single lock.
- Eight workers and a single slot drop 80 ticks to 31 on the I/O-bound load — gain 49 ticks, ratio 0.6125. The same mechanism drops 80 to only 73 on the CPU-bound load — gain 7 ticks. The difference is exactly sevenfold.
- Gain can never exceed the number of overlappable steps: on the CPU-bound load, I/O steps are 7 and gain is 7.
- On the CPU-bound load, the regime sits exactly on the lower bound (73 against 73); on the I/O-bound load, a 5-tick gap remains, and the slot sits empty in those five ticks.
- Adding workers earns until overlappable steps run out: on the CPU-bound load, gain freezes at the second worker; on the I/O-bound load, it keeps earning, diminishingly, through the eighth.
Next Step
This lesson’s last number shows a boundary: on the CPU-bound load, no matter how many workers get added, tick count does not drop below 73, because the setting that does not change is the slot. The next lesson changes that setting. Starting a separate process means a separate interpreter, and a separate interpreter means a separate slot. Its question: how far does four slots drop eighty steps on the CPU-bound load, and once slot count equals task count, does load composition still say anything?
To keep your progress and take notes, Log in
My notes
Log in to take notes.