The simulation below shows 3 readers continuously reading a shared configuration value and a single writer that publishes new versions using the RCU pattern. Press Publish update to trigger a write: the writer copies the current value, edits the copy, and swaps the pointer. Readers already in flight finish on the old version; new readers pick up the new one immediately.
<!-- RCU demo layout — writer column left, readers column right, log below -->
<p class="hint">RCU lets readers proceed without any locks. Each reader takes a snapshot of the shared pointer at the start of its critical section and reads straight through. A writer copies the old version, edits privately, then atomically swaps the pointer. Old readers finish on the old version; new readers see the new one.</p>
<div class="scene">
<div class="col">
<div class="col-label">Writer</div>
<div id="writer-box" class="actor writer-actor">
<div class="actor-name">Writer thread</div>
<div id="writer-status" class="actor-status">idle</div>
</div>
<button id="btn-publish" type="button">Publish update</button>
<div class="shared-box">
<div class="shared-label">shared pointer</div>
<div id="shared-ptr" class="ptr-value">v1</div>
</div>
</div>
<div class="col readers-col">
<div class="col-label">Readers (3)</div>
<div id="readers" class="readers"></div>
</div>
</div>
<div id="log" class="log"></div>
<button id="btn-reset" type="button" class="ghost">Reset</button>
/* RCU demo styles — writer=blue, readers=yellow, old-version readers=red */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.scene { display: flex; gap: 1.2rem; margin-bottom: .6rem; }
.col { display: flex; flex-direction: column; align-items: center; gap: .5rem; }
.readers-col { flex: 1; }
.col-label { font-size: .75rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #666; }
.actor { border-radius: 10px; padding: .5rem .7rem; min-width: 110px; text-align: center; }
.writer-actor { background: #dbeafe; border: 2px solid #3b82f6; }
.actor-name { font-weight: 700; font-size: .85rem; }
.actor-status { font-size: .78rem; color: #555; margin-top: .15rem; min-height: 1.1em; }
.shared-box { background: #f0fdf4; border: 2px solid #22c55e; border-radius: 10px;
padding: .4rem .7rem; text-align: center; min-width: 100px; }
.shared-label { font-size: .72rem; color: #166534; font-weight: 700; text-transform: uppercase; }
.ptr-value { font: 700 1.3rem ui-monospace, monospace; color: #15803d; }
.readers { display: flex; flex-direction: column; gap: .4rem; width: 100%; }
.reader { background: #fef9c3; border: 2px solid #eab308; border-radius: 8px;
padding: .35rem .6rem; display: flex; justify-content: space-between;
align-items: center; transition: background .3s; }
.reader.active { background: #fde68a; }
.reader.old-ver { background: #fee2e2; border-color: #f87171; }
.reader-name { font-weight: 700; font-size: .82rem; }
.reader-info { font-size: .78rem; color: #555; }
.log { height: 90px; overflow-y: auto; background: #f8f9fa; border: 1px solid #dee2e6;
border-radius: 6px; padding: .4rem .6rem; font-size: .78rem; font-family: ui-monospace, monospace;
color: #333; margin-bottom: .5rem; }
.log-line { padding: .05rem 0; }
.log-line.write { color: #1d4ed8; }
.log-line.read { color: #065f46; }
.log-line.reclaim { color: #7c3aed; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-top: .1rem; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// RCU simulation — readers tick every TICK_MS, writer goes through copy/modify/swap/reclaim
// RCU: readers snapshot the pointer at read-start and proceed without locks.
// Writer: (1) copy old data, (2) modify copy privately, (3) atomic pointer swap,
// (4) wait grace period until no reader holds old ref, then reclaim old copy.
let version = 1;
// shared pointer string — represents the globally visible pointer
let sharedPtr = 'v' + version;
let publishCount = 0;
const READER_COUNT = 3;
// ms each reader spends inside its critical section
const READ_DURATION_MS = 1800;
// simulation tick interval in ms
const TICK_MS = 120;
const logEl = document.getElementById('log');
const ptrEl = document.getElementById('shared-ptr');
const writerStatusEl = document.getElementById('writer-status');
const readersEl = document.getElementById('readers');
const btnPublish = document.getElementById('btn-publish');
const btnReset = document.getElementById('btn-reset');
function addLog(msg, cls) {
const d = document.createElement('div');
d.className = 'log-line ' + cls;
d.textContent = msg;
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
}
// reader state: id, which version it started reading, progress through read
const readers = Array.from({ length: READER_COUNT }, (_, i) => ({
id: i + 1,
seenVersion: sharedPtr,
readStart: 0,
progress: 0,
el: null,
infoEl: null,
}));
function buildReaders() {
readersEl.innerHTML = '';
readers.forEach(r => {
const div = document.createElement('div');
div.className = 'reader';
div.id = 'reader-' + r.id;
const nameSpan = document.createElement('span');
nameSpan.className = 'reader-name';
nameSpan.textContent = i18nFmt('Reader %n%', { n: r.id });
const infoSpan = document.createElement('span');
infoSpan.className = 'reader-info';
infoSpan.textContent = '—';
div.appendChild(nameSpan);
div.appendChild(infoSpan);
readersEl.appendChild(div);
r.el = div;
r.infoEl = infoSpan;
});
}
// simulation tick: advance every reader by TICK_MS
let ticker = null;
let now = 0;
function tick() {
now += TICK_MS;
readers.forEach(r => {
// advance this reader's progress through the current read cycle
r.progress += TICK_MS;
if (r.progress >= READ_DURATION_MS) {
// reader finished its critical section — snapshot a new version for next cycle
r.progress = 0;
const wasVersion = r.seenVersion;
// snapshot the current pointer for the next read cycle
r.seenVersion = sharedPtr;
r.el.className = 'reader';
r.infoEl.textContent = i18nFmt('read %ver%', { ver: wasVersion });
addLog(i18nFmt('R%n% finished reading %ver%', { n: r.id, ver: wasVersion }), 'read');
} else {
// reader still inside critical section — show progress bar pct
const pct = Math.floor((r.progress / READ_DURATION_MS) * 100);
const isOld = r.seenVersion !== sharedPtr;
r.el.className = 'reader active' + (isOld ? ' old-ver' : '');
r.infoEl.textContent = i18nFmt('reading %ver% … %pct%%', { ver: r.seenVersion, pct: pct });
}
});
}
function startTicker() {
if (ticker) return;
ticker = setInterval(tick, TICK_MS);
}
function publish() {
btnPublish.disabled = true;
const oldPtr = sharedPtr;
const newVersion = ++version;
const newPtr = 'v' + newVersion;
publishCount++;
// step 1: copy
writerStatusEl.textContent = i18nFmt('copying %old% …', { old: oldPtr });
addLog(i18nFmt('Writer: copy %old% → %n%', { old: oldPtr, n: newPtr }), 'write');
setTimeout(() => {
// step 2: modify the private copy
writerStatusEl.textContent = i18nFmt('editing %n% (private) …', { n: newPtr });
addLog(i18nFmt('Writer: modify %n% (private copy)', { n: newPtr }), 'write');
setTimeout(() => {
// step 3: atomic swap — all future readers see the new version
sharedPtr = newPtr;
ptrEl.textContent = newPtr;
writerStatusEl.textContent = i18nFmt('swapped → %n%', { n: newPtr });
addLog(i18nFmt('Writer: atomic swap → shared pointer = %n%', { n: newPtr }), 'write');
// step 4: wait for grace period before reclaiming old version
setTimeout(() => {
writerStatusEl.textContent = i18nFmt('grace period … reclaim %old%', { old: oldPtr });
addLog(i18nFmt('Writer: grace period elapsed, reclaim %old%', { old: oldPtr }), 'reclaim');
setTimeout(() => {
writerStatusEl.textContent = 'idle';
btnPublish.disabled = false;
}, 400);
}, READ_DURATION_MS + 200);
}, 500);
}, 500);
}
function reset() {
clearInterval(ticker);
ticker = null;
version = 1;
sharedPtr = 'v1';
publishCount = 0;
now = 0;
ptrEl.textContent = 'v1';
writerStatusEl.textContent = 'idle';
logEl.innerHTML = '';
btnPublish.disabled = false;
readers.forEach(r => {
r.seenVersion = sharedPtr;
r.progress = Math.random() * READ_DURATION_MS;
r.el.className = 'reader';
r.infoEl.textContent = '—';
});
buildReaders();
startTicker();
addLog('Simulation reset.', 'reclaim');
}
btnPublish.addEventListener('click', publish);
btnReset.addEventListener('click', reset);
buildReaders();
// stagger reader start offsets so they are not all in sync
readers.forEach((r, i) => { r.progress = Math.floor(i * (READ_DURATION_MS / READER_COUNT)); });
startTicker();
addLog('Simulation started. Readers running.', 'read');
<!-- RCU demo layout — writer column left, readers column right, log below -->
<p class="hint">RCU lets readers proceed without any locks. Each reader takes a snapshot of the shared pointer at the start of its critical section and reads straight through. A writer copies the old version, edits privately, then atomically swaps the pointer. Old readers finish on the old version; new readers see the new one.</p>
<div class="scene">
<div class="col">
<div class="col-label">Writer</div>
<div id="writer-box" class="actor writer-actor">
<div class="actor-name">Writer thread</div>
<div id="writer-status" class="actor-status">idle</div>
</div>
<button id="btn-publish" type="button">Publish update</button>
<div class="shared-box">
<div class="shared-label">shared pointer</div>
<div id="shared-ptr" class="ptr-value">v1</div>
</div>
</div>
<div class="col readers-col">
<div class="col-label">Readers (3)</div>
<div id="readers" class="readers"></div>
</div>
</div>
<div id="log" class="log"></div>
<button id="btn-reset" type="button" class="ghost">Reset</button>
/* RCU demo styles — writer=blue, readers=yellow, old-version readers=red */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.scene { display: flex; gap: 1.2rem; margin-bottom: .6rem; }
.col { display: flex; flex-direction: column; align-items: center; gap: .5rem; }
.readers-col { flex: 1; }
.col-label { font-size: .75rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #666; }
.actor { border-radius: 10px; padding: .5rem .7rem; min-width: 110px; text-align: center; }
.writer-actor { background: #dbeafe; border: 2px solid #3b82f6; }
.actor-name { font-weight: 700; font-size: .85rem; }
.actor-status { font-size: .78rem; color: #555; margin-top: .15rem; min-height: 1.1em; }
.shared-box { background: #f0fdf4; border: 2px solid #22c55e; border-radius: 10px;
padding: .4rem .7rem; text-align: center; min-width: 100px; }
.shared-label { font-size: .72rem; color: #166534; font-weight: 700; text-transform: uppercase; }
.ptr-value { font: 700 1.3rem ui-monospace, monospace; color: #15803d; }
.readers { display: flex; flex-direction: column; gap: .4rem; width: 100%; }
.reader { background: #fef9c3; border: 2px solid #eab308; border-radius: 8px;
padding: .35rem .6rem; display: flex; justify-content: space-between;
align-items: center; transition: background .3s; }
.reader.active { background: #fde68a; }
.reader.old-ver { background: #fee2e2; border-color: #f87171; }
.reader-name { font-weight: 700; font-size: .82rem; }
.reader-info { font-size: .78rem; color: #555; }
.log { height: 90px; overflow-y: auto; background: #f8f9fa; border: 1px solid #dee2e6;
border-radius: 6px; padding: .4rem .6rem; font-size: .78rem; font-family: ui-monospace, monospace;
color: #333; margin-bottom: .5rem; }
.log-line { padding: .05rem 0; }
.log-line.write { color: #1d4ed8; }
.log-line.read { color: #065f46; }
.log-line.reclaim { color: #7c3aed; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; margin-top: .1rem; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// RCU simulation — readers tick every TICK_MS, writer goes through copy/modify/swap/reclaim
// RCU: readers snapshot the pointer at read-start and proceed without locks.
// Writer: (1) copy old data, (2) modify copy privately, (3) atomic pointer swap,
// (4) wait grace period until no reader holds old ref, then reclaim old copy.
let version = 1;
// shared pointer string — represents the globally visible pointer
let sharedPtr = 'v' + version;
let publishCount = 0;
const READER_COUNT = 3;
// ms each reader spends inside its critical section
const READ_DURATION_MS = 1800;
// simulation tick interval in ms
const TICK_MS = 120;
const logEl = document.getElementById('log');
const ptrEl = document.getElementById('shared-ptr');
const writerStatusEl = document.getElementById('writer-status');
const readersEl = document.getElementById('readers');
const btnPublish = document.getElementById('btn-publish');
const btnReset = document.getElementById('btn-reset');
function addLog(msg, cls) {
const d = document.createElement('div');
d.className = 'log-line ' + cls;
d.textContent = msg;
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
}
// reader state: id, which version it started reading, progress through read
const readers = Array.from({ length: READER_COUNT }, (_, i) => ({
id: i + 1,
seenVersion: sharedPtr,
readStart: 0,
progress: 0,
el: null,
infoEl: null,
}));
function buildReaders() {
readersEl.innerHTML = '';
readers.forEach(r => {
const div = document.createElement('div');
div.className = 'reader';
div.id = 'reader-' + r.id;
const nameSpan = document.createElement('span');
nameSpan.className = 'reader-name';
nameSpan.textContent = i18nFmt('Reader %n%', { n: r.id });
const infoSpan = document.createElement('span');
infoSpan.className = 'reader-info';
infoSpan.textContent = '—';
div.appendChild(nameSpan);
div.appendChild(infoSpan);
readersEl.appendChild(div);
r.el = div;
r.infoEl = infoSpan;
});
}
// simulation tick: advance every reader by TICK_MS
let ticker = null;
let now = 0;
function tick() {
now += TICK_MS;
readers.forEach(r => {
// advance this reader's progress through the current read cycle
r.progress += TICK_MS;
if (r.progress >= READ_DURATION_MS) {
// reader finished its critical section — snapshot a new version for next cycle
r.progress = 0;
const wasVersion = r.seenVersion;
// snapshot the current pointer for the next read cycle
r.seenVersion = sharedPtr;
r.el.className = 'reader';
r.infoEl.textContent = i18nFmt('read %ver%', { ver: wasVersion });
addLog(i18nFmt('R%n% finished reading %ver%', { n: r.id, ver: wasVersion }), 'read');
} else {
// reader still inside critical section — show progress bar pct
const pct = Math.floor((r.progress / READ_DURATION_MS) * 100);
const isOld = r.seenVersion !== sharedPtr;
r.el.className = 'reader active' + (isOld ? ' old-ver' : '');
r.infoEl.textContent = i18nFmt('reading %ver% … %pct%%', { ver: r.seenVersion, pct: pct });
}
});
}
function startTicker() {
if (ticker) return;
ticker = setInterval(tick, TICK_MS);
}
function publish() {
btnPublish.disabled = true;
const oldPtr = sharedPtr;
const newVersion = ++version;
const newPtr = 'v' + newVersion;
publishCount++;
// step 1: copy
writerStatusEl.textContent = i18nFmt('copying %old% …', { old: oldPtr });
addLog(i18nFmt('Writer: copy %old% → %n%', { old: oldPtr, n: newPtr }), 'write');
setTimeout(() => {
// step 2: modify the private copy
writerStatusEl.textContent = i18nFmt('editing %n% (private) …', { n: newPtr });
addLog(i18nFmt('Writer: modify %n% (private copy)', { n: newPtr }), 'write');
setTimeout(() => {
// step 3: atomic swap — all future readers see the new version
sharedPtr = newPtr;
ptrEl.textContent = newPtr;
writerStatusEl.textContent = i18nFmt('swapped → %n%', { n: newPtr });
addLog(i18nFmt('Writer: atomic swap → shared pointer = %n%', { n: newPtr }), 'write');
// step 4: wait for grace period before reclaiming old version
setTimeout(() => {
writerStatusEl.textContent = i18nFmt('grace period … reclaim %old%', { old: oldPtr });
addLog(i18nFmt('Writer: grace period elapsed, reclaim %old%', { old: oldPtr }), 'reclaim');
setTimeout(() => {
writerStatusEl.textContent = 'idle';
btnPublish.disabled = false;
}, 400);
}, READ_DURATION_MS + 200);
}, 500);
}, 500);
}
function reset() {
clearInterval(ticker);
ticker = null;
version = 1;
sharedPtr = 'v1';
publishCount = 0;
now = 0;
ptrEl.textContent = 'v1';
writerStatusEl.textContent = 'idle';
logEl.innerHTML = '';
btnPublish.disabled = false;
readers.forEach(r => {
r.seenVersion = sharedPtr;
r.progress = Math.random() * READ_DURATION_MS;
r.el.className = 'reader';
r.infoEl.textContent = '—';
});
buildReaders();
startTicker();
addLog('Simulation reset.', 'reclaim');
}
btnPublish.addEventListener('click', publish);
btnReset.addEventListener('click', reset);
buildReaders();
// stagger reader start offsets so they are not all in sync
readers.forEach((r, i) => { r.progress = Math.floor(i * (READ_DURATION_MS / READER_COUNT)); });
startTicker();
addLog('Simulation started. Readers running.', 'read');
Notice that no reader ever pauses — each one reads straight through without acquiring any lock. The writer does all the coordination work: copy, modify, swap. The old version lingers briefly until all readers that held a reference to it have moved on (the "grace period"), then it is reclaimed safely.
Comments
Loading comments...