The demo below simulates the two paths a futex takes. Add threads and watch what happens: an uncontended acquire completes in userspace with one atomic op; only when threads collide does the simulation show a kernel WAIT syscall parking the loser.
<!-- Futex simulator — shows fast (userspace) vs slow (kernel) lock acquisition paths -->
<div class="controls">
<label for="thread-count">Threads</label>
<input id="thread-count" type="range" min="1" max="8" value="1" />
<span id="thread-val">1</span>
<button id="btn-run" type="button">Run simulation</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="stats-row">
<div class="stat-box">
<div class="stat-label">Kernel calls</div>
<div class="stat-value" id="kernel-calls">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Userspace acquires</div>
<div class="stat-value" id="userspace-acquires">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Total acquires</div>
<div class="stat-value" id="total-acquires">0</div>
</div>
</div>
<div class="lane-area" id="lane-area"></div>
<div class="log" id="log"></div>
/* Styles for the futex demo — lane bars, stat boxes, log */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-size: .85rem; font-weight: 600; }
input[type=range] { width: 120px; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats-row { display: flex; gap: .7rem; margin-bottom: .8rem; flex-wrap: wrap; }
.stat-box { background: #f0f4f8; border-radius: 8px; padding: .4rem .8rem; min-width: 100px; }
.stat-label { font-size: .72rem; color: #556; text-transform: uppercase; letter-spacing: .03em; }
.stat-value { font-size: 1.5rem; font-weight: 700; color: #1d3557; }
.lane-area { display: flex; flex-direction: column; gap: 6px; margin-bottom: .8rem; }
.lane { display: flex; align-items: center; gap: 8px; font-size: .82rem; }
.lane-label { width: 64px; font-weight: 600; color: #334; }
.track { flex: 1; height: 20px; background: #e2e8f0; border-radius: 10px; position: relative; overflow: hidden; }
.fill { height: 100%; border-radius: 10px; width: 0; transition: width .3s; }
.fill.fast { background: #2ecc71; }
.fill.slow { background: #e67e22; }
.badge { font-size: .7rem; font-weight: 700; color: #fff; padding: 1px 6px; border-radius: 8px; margin-left: 4px; }
.badge.kernel { background: #e74c3c; }
.badge.user { background: #27ae60; }
.log { font: .78rem ui-monospace, monospace; color: #334; max-height: 110px; overflow-y: auto;
background: #f8f9fa; border-radius: 6px; padding: .4rem .6rem; line-height: 1.6; }
.log .kline { color: #c0392b; }
.log .uline { color: #27ae60; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Futex simulation: models the two acquisition paths of a fast userspace mutex.
// Fast path: atomic CAS in userspace — no syscall needed when lock is free.
// Slow path: FUTEX_WAIT syscall parks the thread when lock is held by another.
const LOCK_FREE = 0;
const LOCK_HELD = 1;
const LOCK_CONTENDED = 2; // lock held with waiters queued
const ITERATIONS = 12; // lock acquisitions per thread in this simulation
let kernelCalls = 0;
let userAcquires = 0;
let totalAcquires = 0;
let running = false;
function updateStats() {
document.getElementById('kernel-calls').textContent = kernelCalls;
document.getElementById('userspace-acquires').textContent = userAcquires;
document.getElementById('total-acquires').textContent = totalAcquires;
}
function addLog(msg, cls) {
const log = document.getElementById('log');
const d = document.createElement('div');
d.className = cls || '';
d.textContent = msg;
log.appendChild(d);
log.scrollTop = log.scrollHeight;
}
function resetUI() {
kernelCalls = 0; userAcquires = 0; totalAcquires = 0;
running = false;
updateStats();
document.getElementById('log').innerHTML = '';
document.getElementById('lane-area').innerHTML = '';
document.getElementById('btn-run').disabled = false;
}
// Run a step-by-step simulation of nThreads competing for one futex lock.
function simulateFutex(nThreads) {
if (running) return;
running = true;
kernelCalls = 0; userAcquires = 0; totalAcquires = 0;
updateStats();
document.getElementById('log').innerHTML = '';
document.getElementById('btn-run').disabled = true;
const laneArea = document.getElementById('lane-area');
laneArea.innerHTML = '';
// Shared 32-bit lock word: 0=free, 1=held, 2=contended.
let lockWord = LOCK_FREE;
const lanes = [];
for (let i = 0; i < nThreads; i++) {
const lane = document.createElement('div');
lane.className = 'lane';
lane.innerHTML = `<div class="lane-label">Thread ${i + 1}</div>
<div class="track"><div class="fill" id="fill-${i}"></div></div>
<span class="badge" id="badge-${i}"></span>`;
laneArea.appendChild(lane);
lanes.push({ fill: document.getElementById('fill-' + i), badge: document.getElementById('badge-' + i) });
}
// Simulated kernel wait queue (parked thread IDs)
let waitQueue = [];
let step = 0;
const totalSteps = nThreads * ITERATIONS;
// Advance one acquisition step; alternate between threads round-robin.
function tick() {
if (step >= totalSteps) {
running = false;
document.getElementById('btn-run').disabled = false;
addLog('Simulation complete.');
return;
}
const tid = step % nThreads;
const progress = Math.round(((step + 1) / totalSteps) * 100);
const fill = lanes[tid].fill;
const badge = lanes[tid].badge;
fill.style.width = progress + '%';
// Simulate CAS: if lock is free, acquire in userspace (fast path).
if (lockWord === LOCK_FREE) {
// Fast path acquired — no kernel call needed.
lockWord = LOCK_HELD;
userAcquires++;
totalAcquires++;
fill.className = 'fill fast';
badge.className = 'badge user';
badge.textContent = 'USER';
addLog(i18nFmt('Thread %tid%: CAS succeeded → acquired in userspace (no syscall).', { tid: tid + 1 }), 'uline');
// Release: if waiters exist, call FUTEX_WAKE (kernel); otherwise just write 0 back.
if (waitQueue.length === 0) {
lockWord = LOCK_FREE;
} else {
lockWord = LOCK_CONTENDED;
kernelCalls++;
updateStats();
addLog(i18nFmt('Thread %tid%: released → FUTEX_WAKE woke thread %woken%.', { tid: tid + 1, woken: waitQueue.shift() + 1 }), 'kline');
lockWord = LOCK_FREE;
}
} else {
// Lock is held — slow path: call FUTEX_WAIT to park this thread in the kernel.
lockWord = LOCK_CONTENDED;
kernelCalls++;
totalAcquires++;
fill.className = 'fill slow';
badge.className = 'badge kernel';
badge.textContent = 'KERNEL';
waitQueue.push(tid);
addLog(i18nFmt('Thread %tid%: CAS failed → FUTEX_WAIT (kernel parked thread).', { tid: tid + 1 }), 'kline');
}
updateStats();
step++;
setTimeout(tick, 280);
}
tick();
}
document.getElementById('thread-count').addEventListener('input', function () {
document.getElementById('thread-val').textContent = this.value;
});
document.getElementById('btn-run').addEventListener('click', function () {
const n = parseInt(document.getElementById('thread-count').value, 10);
simulateFutex(n);
});
document.getElementById('btn-reset').addEventListener('click', resetUI);
<!-- Futex simulator — shows fast (userspace) vs slow (kernel) lock acquisition paths -->
<div class="controls">
<label for="thread-count">Threads</label>
<input id="thread-count" type="range" min="1" max="8" value="1" />
<span id="thread-val">1</span>
<button id="btn-run" type="button">Run simulation</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="stats-row">
<div class="stat-box">
<div class="stat-label">Kernel calls</div>
<div class="stat-value" id="kernel-calls">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Userspace acquires</div>
<div class="stat-value" id="userspace-acquires">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Total acquires</div>
<div class="stat-value" id="total-acquires">0</div>
</div>
</div>
<div class="lane-area" id="lane-area"></div>
<div class="log" id="log"></div>
/* Styles for the futex demo — lane bars, stat boxes, log */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-size: .85rem; font-weight: 600; }
input[type=range] { width: 120px; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats-row { display: flex; gap: .7rem; margin-bottom: .8rem; flex-wrap: wrap; }
.stat-box { background: #f0f4f8; border-radius: 8px; padding: .4rem .8rem; min-width: 100px; }
.stat-label { font-size: .72rem; color: #556; text-transform: uppercase; letter-spacing: .03em; }
.stat-value { font-size: 1.5rem; font-weight: 700; color: #1d3557; }
.lane-area { display: flex; flex-direction: column; gap: 6px; margin-bottom: .8rem; }
.lane { display: flex; align-items: center; gap: 8px; font-size: .82rem; }
.lane-label { width: 64px; font-weight: 600; color: #334; }
.track { flex: 1; height: 20px; background: #e2e8f0; border-radius: 10px; position: relative; overflow: hidden; }
.fill { height: 100%; border-radius: 10px; width: 0; transition: width .3s; }
.fill.fast { background: #2ecc71; }
.fill.slow { background: #e67e22; }
.badge { font-size: .7rem; font-weight: 700; color: #fff; padding: 1px 6px; border-radius: 8px; margin-left: 4px; }
.badge.kernel { background: #e74c3c; }
.badge.user { background: #27ae60; }
.log { font: .78rem ui-monospace, monospace; color: #334; max-height: 110px; overflow-y: auto;
background: #f8f9fa; border-radius: 6px; padding: .4rem .6rem; line-height: 1.6; }
.log .kline { color: #c0392b; }
.log .uline { color: #27ae60; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Futex simulation: models the two acquisition paths of a fast userspace mutex.
// Fast path: atomic CAS in userspace — no syscall needed when lock is free.
// Slow path: FUTEX_WAIT syscall parks the thread when lock is held by another.
const LOCK_FREE = 0;
const LOCK_HELD = 1;
const LOCK_CONTENDED = 2; // lock held with waiters queued
const ITERATIONS = 12; // lock acquisitions per thread in this simulation
let kernelCalls = 0;
let userAcquires = 0;
let totalAcquires = 0;
let running = false;
function updateStats() {
document.getElementById('kernel-calls').textContent = kernelCalls;
document.getElementById('userspace-acquires').textContent = userAcquires;
document.getElementById('total-acquires').textContent = totalAcquires;
}
function addLog(msg, cls) {
const log = document.getElementById('log');
const d = document.createElement('div');
d.className = cls || '';
d.textContent = msg;
log.appendChild(d);
log.scrollTop = log.scrollHeight;
}
function resetUI() {
kernelCalls = 0; userAcquires = 0; totalAcquires = 0;
running = false;
updateStats();
document.getElementById('log').innerHTML = '';
document.getElementById('lane-area').innerHTML = '';
document.getElementById('btn-run').disabled = false;
}
// Run a step-by-step simulation of nThreads competing for one futex lock.
function simulateFutex(nThreads) {
if (running) return;
running = true;
kernelCalls = 0; userAcquires = 0; totalAcquires = 0;
updateStats();
document.getElementById('log').innerHTML = '';
document.getElementById('btn-run').disabled = true;
const laneArea = document.getElementById('lane-area');
laneArea.innerHTML = '';
// Shared 32-bit lock word: 0=free, 1=held, 2=contended.
let lockWord = LOCK_FREE;
const lanes = [];
for (let i = 0; i < nThreads; i++) {
const lane = document.createElement('div');
lane.className = 'lane';
lane.innerHTML = `<div class="lane-label">Thread ${i + 1}</div>
<div class="track"><div class="fill" id="fill-${i}"></div></div>
<span class="badge" id="badge-${i}"></span>`;
laneArea.appendChild(lane);
lanes.push({ fill: document.getElementById('fill-' + i), badge: document.getElementById('badge-' + i) });
}
// Simulated kernel wait queue (parked thread IDs)
let waitQueue = [];
let step = 0;
const totalSteps = nThreads * ITERATIONS;
// Advance one acquisition step; alternate between threads round-robin.
function tick() {
if (step >= totalSteps) {
running = false;
document.getElementById('btn-run').disabled = false;
addLog('Simulation complete.');
return;
}
const tid = step % nThreads;
const progress = Math.round(((step + 1) / totalSteps) * 100);
const fill = lanes[tid].fill;
const badge = lanes[tid].badge;
fill.style.width = progress + '%';
// Simulate CAS: if lock is free, acquire in userspace (fast path).
if (lockWord === LOCK_FREE) {
// Fast path acquired — no kernel call needed.
lockWord = LOCK_HELD;
userAcquires++;
totalAcquires++;
fill.className = 'fill fast';
badge.className = 'badge user';
badge.textContent = 'USER';
addLog(i18nFmt('Thread %tid%: CAS succeeded → acquired in userspace (no syscall).', { tid: tid + 1 }), 'uline');
// Release: if waiters exist, call FUTEX_WAKE (kernel); otherwise just write 0 back.
if (waitQueue.length === 0) {
lockWord = LOCK_FREE;
} else {
lockWord = LOCK_CONTENDED;
kernelCalls++;
updateStats();
addLog(i18nFmt('Thread %tid%: released → FUTEX_WAKE woke thread %woken%.', { tid: tid + 1, woken: waitQueue.shift() + 1 }), 'kline');
lockWord = LOCK_FREE;
}
} else {
// Lock is held — slow path: call FUTEX_WAIT to park this thread in the kernel.
lockWord = LOCK_CONTENDED;
kernelCalls++;
totalAcquires++;
fill.className = 'fill slow';
badge.className = 'badge kernel';
badge.textContent = 'KERNEL';
waitQueue.push(tid);
addLog(i18nFmt('Thread %tid%: CAS failed → FUTEX_WAIT (kernel parked thread).', { tid: tid + 1 }), 'kline');
}
updateStats();
step++;
setTimeout(tick, 280);
}
tick();
}
document.getElementById('thread-count').addEventListener('input', function () {
document.getElementById('thread-val').textContent = this.value;
});
document.getElementById('btn-run').addEventListener('click', function () {
const n = parseInt(document.getElementById('thread-count').value, 10);
simulateFutex(n);
});
document.getElementById('btn-reset').addEventListener('click', resetUI);
Notice the kernel calls counter. In the uncontended case it stays at zero — the lock is acquired and released entirely in userspace. Add more threads and contention rises: the kernel must now park waiting threads and wake them up, at a cost visible in the call count. The fast path is the whole point of the design.
Comments
Loading comments...