Below, four threads compete for a critical section. Click Step to advance one thread at a time, or Run to let them race. Each thread picks the next ticket number, waits until it holds the smallest ticket, then enters alone.
<p class="hint">Four threads compete for one critical section. Each picks the next ticket number and waits until it holds the smallest — just like a bakery queue. Click <b>Step</b> to advance one action, or <b>Run</b> to watch them race.</p>
<div id="threads-row"></div>
<div id="cs-box">
<span id="cs-label">Critical section</span>
<span id="cs-occupant">empty</span>
</div>
<div class="log-wrap"><div id="log"></div></div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
#threads-row { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.thread-card {
width: 96px; border-radius: 10px; padding: 8px 6px; text-align: center;
border: 2px solid #c6cdd5; background: #f1f4f7; transition: all .25s;
font-size: .8rem; line-height: 1.6;
}
.thread-card .tid { font-weight: 700; font-size: 1rem; }
.thread-card .ticket { color: #1d3557; font-size: 1.1rem; font-weight: 700; }
.thread-card .phase { color: #666; font-size: .72rem; }
.thread-card.choosing { border-color: #f4a261; background: #fff4ec; }
.thread-card.waiting { border-color: #457b9d; background: #e8f0f7; }
.thread-card.inside { border-color: #2a9d8f; background: #e4f5f3; }
.thread-card.done { border-color: #aaa; background: #f7f7f7; opacity: .6; }
#cs-box {
display: flex; align-items: center; gap: 14px;
border: 2px dashed #2a9d8f; border-radius: 10px;
padding: 10px 14px; margin-bottom: 10px;
}
#cs-label { font-size: .8rem; color: #555; }
#cs-occupant { font-weight: 700; font-size: 1.1rem; color: #1d3557; }
#cs-occupant.occupied { color: #2a9d8f; }
.log-wrap { height: 110px; overflow-y: auto; background: #f8fafc;
border: 1px solid #dde3ea; border-radius: 8px; padding: 6px 8px;
margin-bottom: 10px; font-size: .78rem; line-height: 1.6; }
#log p { margin: 0; color: #444; }
#log p.hi { color: #1d3557; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lamport's Bakery Algorithm (1974).
// Shared state for N threads:
// choosing[i] – true while thread i is in the "choosing" phase
// number[i] – ticket number (0 = not participating)
// Entry protocol for thread i:
// 1. choosing[i] = true
// 2. number[i] = 1 + max(number[0..N-1])
// 3. choosing[i] = false
// 4. for each j != i:
// wait while choosing[j]
// wait while number[j] != 0 &&
// (number[j] < number[i] ||
// (number[j] == number[i] && j < i))
// Exit: number[i] = 0
const N = 4;
const WORK_ROUNDS = 2; // steps each thread spends "in" the CS before leaving
// Shared memory
let choosing, number;
// Per-thread simulation state
// phase: 'idle' | 'choosing' | 'waiting' | 'inside' | 'done'
let threads, csOccupant, entryCount, runInterval;
function initState() {
choosing = new Array(N).fill(false);
number = new Array(N).fill(0);
threads = Array.from({length: N}, (_, i) => ({
id: i, phase: 'idle', waitFor: 0, workLeft: 0,
entries: 0, checkJ: 0
}));
csOccupant = -1;
entryCount = 0;
}
// Returns the next logical action for thread i, as a {done, log, hi} object. Mutates shared + thread state one step at a time.
function stepThread(i) {
const t = threads[i];
if (t.phase === 'done') return null;
if (t.phase === 'idle') {
// Start entering
choosing[i] = true;
t.phase = 'choosing';
return {log: fmt('T%i% choosing — reading ticket dispenser', { i: i }), hi: false};
}
if (t.phase === 'choosing') {
number[i] = 1 + Math.max(...number);
choosing[i] = false;
t.phase = 'waiting';
t.checkJ = 0; // start checking from thread 0
return {log: fmt('T%i% got ticket #%v%', { i: i, v: number[i] }), hi: true};
}
if (t.phase === 'waiting') {
// Find the next j to check
while (t.checkJ < N && t.checkJ === i) t.checkJ++;
if (t.checkJ >= N) {
// Passed all checks — enter!
csOccupant = i;
t.phase = 'inside';
t.workLeft = WORK_ROUNDS;
entryCount++;
t.entries++;
return {log: fmt('T%i% enters CS (ticket #%v%) — entry #%entryCount%', { i: i, v: number[i], entryCount: entryCount }), hi: true};
}
const j = t.checkJ;
if (choosing[j]) {
return {log: fmt('T%i% waiting: T%j% still choosing', { i: i, j: j }), hi: false};
}
const blocked = number[j] !== 0 &&
(number[j] < number[i] || (number[j] === number[i] && j < i));
if (blocked) {
return {log: fmt('T%i% yields to T%j% (ticket #%v%)', { i: i, j: j, v: number[j] }), hi: false};
}
t.checkJ++;
return {log: fmt('T%i% checked T%j% ✓', { i: i, j: j }), hi: false};
}
if (t.phase === 'inside') {
t.workLeft--;
if (t.workLeft > 0) {
return {log: fmt('T%i% working in CS (%workLeft% step(s) left)', { i: i, workLeft: t.workLeft }), hi: false};
}
// Exit
number[i] = 0;
csOccupant = -1;
t.phase = t.entries >= 2 ? 'done' : 'idle';
return {log: fmt('T%i% exits CS, releases ticket', { i: i }), hi: true};
}
return null;
}
// Pick a thread to advance: prefer non-idle, non-done threads in round-robin
let nextT = 0;
function pickThread() {
// Rotate among active threads; bias toward those in 'choosing' or 'waiting'
const active = threads.filter(t => t.phase !== 'done');
if (!active.length) return -1;
// Prefer the thread with the smallest ticket that is waiting, else round-robin
const waiting = active.filter(t => t.phase === 'waiting' || t.phase === 'inside');
if (waiting.length) {
// advance the one with smallest ticket (tie: smaller id)
waiting.sort((a,b) => (number[a.id]||Infinity) - (number[b.id]||Infinity) || a.id - b.id);
return waiting[0].id;
}
// Idle or choosing — round-robin
const idx = active.findIndex(t => t.id === nextT % N);
const pick = idx >= 0 ? active[idx] : active[0];
nextT = (pick.id + 1) % N;
return pick.id;
}
function logMsg(msg, hi) {
const log = document.getElementById('log');
const p = document.createElement('p');
if (hi) p.className = 'hi';
p.textContent = msg;
log.appendChild(p);
log.scrollTop = log.scrollHeight;
}
function render() {
const row = document.getElementById('threads-row');
row.innerHTML = '';
threads.forEach(t => {
const card = document.createElement('div');
card.className = 'thread-card ' + t.phase;
const tick = number[t.id] > 0 ? `#${number[t.id]}` : '—';
const phaseLabel = {
idle: 'idle', choosing: 'choosing…', waiting: 'waiting',
inside: 'in CS ✓', done: 'done'
}[t.phase];
card.innerHTML =
fmt('<div class="tid">T%id%</div><div class="ticket">%tick%</div><div class="phase">%phaseLabel%</div><div class="phase">entries: %entries%</div>', { id: t.id, tick: tick, phaseLabel: phaseLabel, entries: t.entries });
row.appendChild(card);
});
const occ = document.getElementById('cs-occupant');
if (csOccupant >= 0) {
occ.textContent = fmt('Thread T%csOccupant%', { csOccupant: csOccupant });
occ.className = 'occupied';
} else {
occ.textContent = 'empty';
occ.className = '';
}
const allDone = threads.every(t => t.phase === 'done');
document.getElementById('btn-step').disabled = allDone;
document.getElementById('btn-run').disabled = allDone;
if (allDone) {
logMsg(fmt('All threads finished. Total entries: %entryCount%', { entryCount: entryCount }), true);
stopRun();
}
}
function doStep() {
const i = pickThread();
if (i < 0) return;
const r = stepThread(i);
if (r) logMsg(r.log, r.hi);
render();
}
let running = false;
function startRun() {
if (running) return;
running = true;
document.getElementById('btn-run').textContent = 'Pause';
runInterval = setInterval(doStep, 350);
}
function stopRun() {
running = false;
clearInterval(runInterval);
document.getElementById('btn-run').textContent = 'Run';
}
function reset() {
stopRun();
nextT = 0;
initState();
document.getElementById('log').innerHTML = '';
logMsg('Ready — four threads waiting to enter the critical section.', false);
render();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<p class="hint">Four threads compete for one critical section. Each picks the next ticket number and waits until it holds the smallest — just like a bakery queue. Click <b>Step</b> to advance one action, or <b>Run</b> to watch them race.</p>
<div id="threads-row"></div>
<div id="cs-box">
<span id="cs-label">Critical section</span>
<span id="cs-occupant">empty</span>
</div>
<div class="log-wrap"><div id="log"></div></div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
#threads-row { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
.thread-card {
width: 96px; border-radius: 10px; padding: 8px 6px; text-align: center;
border: 2px solid #c6cdd5; background: #f1f4f7; transition: all .25s;
font-size: .8rem; line-height: 1.6;
}
.thread-card .tid { font-weight: 700; font-size: 1rem; }
.thread-card .ticket { color: #1d3557; font-size: 1.1rem; font-weight: 700; }
.thread-card .phase { color: #666; font-size: .72rem; }
.thread-card.choosing { border-color: #f4a261; background: #fff4ec; }
.thread-card.waiting { border-color: #457b9d; background: #e8f0f7; }
.thread-card.inside { border-color: #2a9d8f; background: #e4f5f3; }
.thread-card.done { border-color: #aaa; background: #f7f7f7; opacity: .6; }
#cs-box {
display: flex; align-items: center; gap: 14px;
border: 2px dashed #2a9d8f; border-radius: 10px;
padding: 10px 14px; margin-bottom: 10px;
}
#cs-label { font-size: .8rem; color: #555; }
#cs-occupant { font-weight: 700; font-size: 1.1rem; color: #1d3557; }
#cs-occupant.occupied { color: #2a9d8f; }
.log-wrap { height: 110px; overflow-y: auto; background: #f8fafc;
border: 1px solid #dde3ea; border-radius: 8px; padding: 6px 8px;
margin-bottom: 10px; font-size: .78rem; line-height: 1.6; }
#log p { margin: 0; color: #444; }
#log p.hi { color: #1d3557; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lamport's Bakery Algorithm (1974).
// Shared state for N threads:
// choosing[i] – true while thread i is in the "choosing" phase
// number[i] – ticket number (0 = not participating)
// Entry protocol for thread i:
// 1. choosing[i] = true
// 2. number[i] = 1 + max(number[0..N-1])
// 3. choosing[i] = false
// 4. for each j != i:
// wait while choosing[j]
// wait while number[j] != 0 &&
// (number[j] < number[i] ||
// (number[j] == number[i] && j < i))
// Exit: number[i] = 0
const N = 4;
const WORK_ROUNDS = 2; // steps each thread spends "in" the CS before leaving
// Shared memory
let choosing, number;
// Per-thread simulation state
// phase: 'idle' | 'choosing' | 'waiting' | 'inside' | 'done'
let threads, csOccupant, entryCount, runInterval;
function initState() {
choosing = new Array(N).fill(false);
number = new Array(N).fill(0);
threads = Array.from({length: N}, (_, i) => ({
id: i, phase: 'idle', waitFor: 0, workLeft: 0,
entries: 0, checkJ: 0
}));
csOccupant = -1;
entryCount = 0;
}
// Returns the next logical action for thread i, as a {done, log, hi} object. Mutates shared + thread state one step at a time.
function stepThread(i) {
const t = threads[i];
if (t.phase === 'done') return null;
if (t.phase === 'idle') {
// Start entering
choosing[i] = true;
t.phase = 'choosing';
return {log: fmt('T%i% choosing — reading ticket dispenser', { i: i }), hi: false};
}
if (t.phase === 'choosing') {
number[i] = 1 + Math.max(...number);
choosing[i] = false;
t.phase = 'waiting';
t.checkJ = 0; // start checking from thread 0
return {log: fmt('T%i% got ticket #%v%', { i: i, v: number[i] }), hi: true};
}
if (t.phase === 'waiting') {
// Find the next j to check
while (t.checkJ < N && t.checkJ === i) t.checkJ++;
if (t.checkJ >= N) {
// Passed all checks — enter!
csOccupant = i;
t.phase = 'inside';
t.workLeft = WORK_ROUNDS;
entryCount++;
t.entries++;
return {log: fmt('T%i% enters CS (ticket #%v%) — entry #%entryCount%', { i: i, v: number[i], entryCount: entryCount }), hi: true};
}
const j = t.checkJ;
if (choosing[j]) {
return {log: fmt('T%i% waiting: T%j% still choosing', { i: i, j: j }), hi: false};
}
const blocked = number[j] !== 0 &&
(number[j] < number[i] || (number[j] === number[i] && j < i));
if (blocked) {
return {log: fmt('T%i% yields to T%j% (ticket #%v%)', { i: i, j: j, v: number[j] }), hi: false};
}
t.checkJ++;
return {log: fmt('T%i% checked T%j% ✓', { i: i, j: j }), hi: false};
}
if (t.phase === 'inside') {
t.workLeft--;
if (t.workLeft > 0) {
return {log: fmt('T%i% working in CS (%workLeft% step(s) left)', { i: i, workLeft: t.workLeft }), hi: false};
}
// Exit
number[i] = 0;
csOccupant = -1;
t.phase = t.entries >= 2 ? 'done' : 'idle';
return {log: fmt('T%i% exits CS, releases ticket', { i: i }), hi: true};
}
return null;
}
// Pick a thread to advance: prefer non-idle, non-done threads in round-robin
let nextT = 0;
function pickThread() {
// Rotate among active threads; bias toward those in 'choosing' or 'waiting'
const active = threads.filter(t => t.phase !== 'done');
if (!active.length) return -1;
// Prefer the thread with the smallest ticket that is waiting, else round-robin
const waiting = active.filter(t => t.phase === 'waiting' || t.phase === 'inside');
if (waiting.length) {
// advance the one with smallest ticket (tie: smaller id)
waiting.sort((a,b) => (number[a.id]||Infinity) - (number[b.id]||Infinity) || a.id - b.id);
return waiting[0].id;
}
// Idle or choosing — round-robin
const idx = active.findIndex(t => t.id === nextT % N);
const pick = idx >= 0 ? active[idx] : active[0];
nextT = (pick.id + 1) % N;
return pick.id;
}
function logMsg(msg, hi) {
const log = document.getElementById('log');
const p = document.createElement('p');
if (hi) p.className = 'hi';
p.textContent = msg;
log.appendChild(p);
log.scrollTop = log.scrollHeight;
}
function render() {
const row = document.getElementById('threads-row');
row.innerHTML = '';
threads.forEach(t => {
const card = document.createElement('div');
card.className = 'thread-card ' + t.phase;
const tick = number[t.id] > 0 ? `#${number[t.id]}` : '—';
const phaseLabel = {
idle: 'idle', choosing: 'choosing…', waiting: 'waiting',
inside: 'in CS ✓', done: 'done'
}[t.phase];
card.innerHTML =
fmt('<div class="tid">T%id%</div><div class="ticket">%tick%</div><div class="phase">%phaseLabel%</div><div class="phase">entries: %entries%</div>', { id: t.id, tick: tick, phaseLabel: phaseLabel, entries: t.entries });
row.appendChild(card);
});
const occ = document.getElementById('cs-occupant');
if (csOccupant >= 0) {
occ.textContent = fmt('Thread T%csOccupant%', { csOccupant: csOccupant });
occ.className = 'occupied';
} else {
occ.textContent = 'empty';
occ.className = '';
}
const allDone = threads.every(t => t.phase === 'done');
document.getElementById('btn-step').disabled = allDone;
document.getElementById('btn-run').disabled = allDone;
if (allDone) {
logMsg(fmt('All threads finished. Total entries: %entryCount%', { entryCount: entryCount }), true);
stopRun();
}
}
function doStep() {
const i = pickThread();
if (i < 0) return;
const r = stepThread(i);
if (r) logMsg(r.log, r.hi);
render();
}
let running = false;
function startRun() {
if (running) return;
running = true;
document.getElementById('btn-run').textContent = 'Pause';
runInterval = setInterval(doStep, 350);
}
function stopRun() {
running = false;
clearInterval(runInterval);
document.getElementById('btn-run').textContent = 'Run';
}
function reset() {
stopRun();
nextT = 0;
initState();
document.getElementById('log').innerHTML = '';
logMsg('Ready — four threads waiting to enter the critical section.', false);
render();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Notice: no thread skips the queue. When two threads grab tickets at the same moment they might get the same number — the algorithm breaks the tie by thread ID. Even with ties, exactly one thread enters the critical section at any instant, and every thread eventually gets its turn.
Comments
Loading comments...