Introduction

Every popular program is eventually asked to do several things at once. The standard answer is a lock: a reader grabs it, does its work, releases it — then a writer gets its turn. Locks are correct, but they force readers to queue even when no write is happening. On a modern server with hundreds of threads, that queue can be the bottleneck.

Read-Copy-Update (RCU) is a different bargain. Readers proceed with zero synchronization overhead — no lock, no atomic increment, nothing. A writer, instead of modifying the shared data in place, makes a copy, edits it privately, then atomically swaps the pointer so future readers see the new version. Old readers finish on the old version; when the last one is done, the old copy is reclaimed.

The result is astonishing: read paths that are as fast as unsynchronized code, with full correctness. RCU was patented by Paul McKenney in 1998 and merged into the Linux kernel in 2002. Today it protects thousands of data structures — routing tables, file-system dentries, network protocol lists — inside every Linux server on the planet.

Try It: Readers Race a Writer

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');
Result
Console

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.

How It Really Works

The magic is real, but the details are delicate.

The three RCU operations:

  • rcu_read_lock() / rcu_read_unlock() — mark the start and end of a read-side critical section. On most architectures this compiles to nothing (or a single preemption-disable).
  • rcu_assign_pointer(p, new) — atomically publishes the new pointer with the necessary memory barrier so readers see a fully initialized object.
  • synchronize_rcu() — the writer calls this after the swap. It blocks until every CPU has passed through a quiescent state (e.g., a context switch or returning from the kernel), guaranteeing that no reader still holds a reference to the old version.

Grace periods and quiescent states: A quiescent state is any moment when a CPU cannot be executing RCU read-side code. Once every CPU has had one quiescent state since the pointer was swapped, the old version is safe to free. This is the grace period.

Why it's fast: The read path has zero atomic operations. The cost is shifted entirely onto the writer via synchronize_rcu(), which may sleep for milliseconds — acceptable because writes are rare compared to reads.

The subtlety: RCU only works when read-side critical sections are bounded and non-blocking. You cannot hold an rcu_read_lock() across a sleep. And you must never dereference a pointer outside an RCU read-side section after the grace period. Getting this wrong is a use-after-free bug — the kind that corrupts memory silently.

Compared to spin locks or read-write locks, RCU is the right tool when reads vastly outnumber writes and bounded latency on reads matters more than write throughput.

Where It Matters

RCU shines wherever the ratio of reads to writes is large and read latency is critical:

  • Linux kernel routing tables: every incoming packet does a route lookup. With RCU, thousands of CPUs look up routes simultaneously with no locking, while routing daemons update the table infrequently in the background.
  • File-system dentry cache: path resolution (open("/etc/passwd")) walks the dentry tree on every call. RCU makes that walk lock-free.
  • Network protocol lists: the list of registered network protocols is read on every received packet and written only when a module loads or unloads.
  • Real-time systems: because rcu_read_lock() never blocks, RCU is one of the few synchronization primitives compatible with hard real-time scheduling — used in the PREEMPT_RT Linux patchset.
  • User-space RCU (liburcu): the same pattern is available outside the kernel for databases, in-memory caches, and lock-free data structures in high-throughput servers.

The principle generalizes: any data structure updated rarely but read constantly — configuration, capability lists, credential caches — is a natural fit for RCU or an RCU-inspired pattern.

Conclusion

Read-Copy-Update is a solved synchronization problem, and its solution is beautiful: move all the coordination cost onto the writer, give readers a completely clear path, and use time (the grace period) instead of locks to guarantee safety.

The idea feels almost too simple — surely readers must pay something? They don't. The Linux kernel's RCU subsystem has proven this at scale for over two decades, protecting the hottest data structures on machines running billions of requests per day.

The deeper lesson is about where you place cost. Every concurrency design makes a tradeoff between reader overhead and writer overhead. RCU makes the most extreme tradeoff possible: zero for readers, all for writers. When reads vastly outnumber writes, that is exactly the right call — and understanding it reframes how you think about concurrency in any system you build.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/read-copy-update/Content licensed under CC BY-NC 4.0.