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.

<!-- {{c_html_intro}} -->
<p class="hint">{{hint_para}}</p>
<div class="scene">
  <div class="col">
    <div class="col-label">{{label_writer}}</div>
    <div id="writer-box" class="actor writer-actor">
      <div class="actor-name">{{writer_name}}</div>
      <div id="writer-status" class="actor-status">{{writer_idle}}</div>
    </div>
    <button id="btn-publish" type="button">{{btn_publish}}</button>
    <div class="shared-box">
      <div class="shared-label">{{label_shared_ptr}}</div>
      <div id="shared-ptr" class="ptr-value">v1</div>
    </div>
  </div>
  <div class="col readers-col">
    <div class="col-label">{{label_readers}}</div>
    <div id="readers" class="readers"></div>
  </div>
</div>
<div id="log" class="log"></div>
<button id="btn-reset" type="button" class="ghost">{{btn_reset}}</button>
/* {{c_css_intro}} */
* { 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; }
// Code not found

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.