codexthread-lost-update-interleaving.py
thread-lost-update-interleaving.py
"""Draw two explicit schedules of an abstract indivisible-step counter model.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media; write the opaque matching PNG to the current directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
def trace(schedule):
"""Return states after each step, keeping each thread's register separate."""
x, registers, next_step = 0, [None, None], [0, 0]
rows = [("Initial", "—", "—", x, None)]
for step, thread in enumerate(schedule, 1):
action = ("load x", "add 1", "store r")[next_step[thread]]
if next_step[thread] == 0:
registers[thread] = x
elif next_step[thread] == 1:
registers[thread] += 1
else:
x = registers[thread]
next_step[thread] += 1
rows.append((f"{step}. T{thread + 1}: {action}",
*["—" if r is None else str(r) for r in registers], x, thread))
assert next_step == [3, 3]
return rows
def main():
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
ink, blue, green, red = "#203040", "#155d8a", "#187355", "#a0392a"
fig, ax = plt.subplots(figsize=(11, 6.3), dpi=110, facecolor="white")
ax.set(xlim=(0, 11), ylim=(0, 6.3), facecolor="white")
ax.axis("off")
ax.text(5.5, 6.04, "Two increments: separate registers, one shared counter",
ha="center", fontsize=17, weight="bold", color=ink)
ax.text(5.5, 5.62, "Abstract model: each load / add / store is indivisible; time runs downward",
ha="center", fontsize=11, color=ink)
panels = [(0.3, "Interleaved: lost update", [0, 1, 0, 1, 0, 1], 1, red),
(5.85, "Serial: T1 then T2", [0, 0, 0, 1, 1, 1], 2, green)]
row_positions = np.linspace(4.27, 1.57, 7)
for left, title, schedule, result, color in panels:
rows = trace(schedule)
assert rows[-1][3] == result
ax.text(left + 2.4, 5.09, title, ha="center", fontsize=14,
weight="bold", color=color)
ax.add_patch(Rectangle((left + 3.86, 1.34), 0.92, 3.5,
facecolor="#fff1dc", edgecolor="none"))
ax.text(left + 0.05, 4.74, "Logical step", color=ink, weight="bold")
for offset, label, label_color in [(2.65, "r1", blue), (3.45, "r2", green),
(4.32, "shared x", ink)]:
ax.text(left + offset, 4.74, label, ha="center", fontsize=11,
color=label_color, weight="bold")
for y, (action, r1, r2, x, thread) in zip(row_positions, rows):
active_color = ink if thread is None else (blue, green)[thread]
ax.plot([left, left + 4.85], [y - 0.23, y - 0.23],
color="#d7dfe5", lw=0.7)
ax.text(left + 0.05, y, action, va="center", color=active_color)
ax.text(left + 2.65, y, r1, ha="center", va="center", color=blue)
ax.text(left + 3.45, y, r2, ha="center", va="center", color=green)
ax.text(left + 4.32, y, str(x), ha="center", va="center",
color=ink, weight="bold")
ax.text(left + 2.4, 0.94, f"Final x = {result}", ha="center", fontsize=15,
weight="bold", color=color)
ax.text(5.5, 0.48, "A load reads the latest preceding store. Adding changes only that thread's register.",
ha="center", fontsize=11, color=ink)
ax.text(5.5, 0.12, "This is not an unprotected C program: C data races have undefined behavior.",
ha="center", fontsize=11, color=ink)
fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"),
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()