Why architecture matters here
The architecture matters because the read side of a seqlock is the fastest correct way to read shared, multi-word data that exists for read-mostly workloads, and the reasons come down to cache coherence. On a multicore machine, the expensive operation is not computation but coordination: when two cores both write to the same cache line, that line ping-pongs between their caches, and each bounce costs tens to hundreds of cycles. A conventional reader-writer lock requires every reader to write to the lock word (to increment a reader count or set a bit), which means N reader threads on N cores all writing the same line — the readers contend with each other even though semantically they do not conflict at all. The seqlock removes that: readers only read the sequence counter and the payload, and reads of a shared-but-unmodified cache line are cheap because every core can hold the line in a shared state simultaneously. A million readers per second on twenty cores add essentially no coordination cost.
The asymmetry the seqlock exploits is the key to when it is the right tool. If reads and writes are balanced, the reader retries become frequent and the whole advantage evaporates — you would be better off with a plain mutex or an RCU scheme. But when writes are genuinely rare (the clock tick, the once-a-minute config reload, the occasional routing update), the probability that any given read races a write is tiny, so retries are vanishingly rare and the amortized read cost is essentially the cost of the data copy plus two counter reads. That is why seqlocks live in the hottest read paths of operating system kernels, where a wrong choice of primitive shows up directly in system-call latency.
The architecture also matters because it is subtly wrong to use casually, and understanding why is what separates a correct seqlock from a corruption generator. Unlike a mutex, a seqlock provides no mutual exclusion to readers — a reader can and will observe a write in progress, seeing some fields updated and others not (a torn read). The protocol guarantees the reader will detect this and retry, but only if the reader treats every field it read as potentially garbage until the post-read counter check passes. A reader that acts on data mid-read — dereferences a pointer, indexes an array with a torn length, divides by a half-written denominator — can crash or corrupt before it ever gets to the validation step. This is why the protected payload must be plain, copyable, self-contained data, and why the seqlock is a scalpel, not a general lock.
Finally, the architecture matters because it is one of the clearest illustrations of why memory ordering is not optional. On a strongly-ordered machine a naive seqlock might appear to work; on a weakly-ordered one (ARM, POWER) the compiler and CPU are free to reorder the payload reads relative to the counter reads, which destroys the entire guarantee — a reader could load the counter, then load payload fields that were actually written after the counter check, and never know. The correct seqlock is defined as much by its fences as by its counter, which makes it a compact lesson in the acquire/release model that governs all lock-free code.
The architecture: every piece explained
The sequence counter is the entire coordination surface: a single unsigned integer, shared, with one invariant — it is even when the data is stable and odd while a write is in progress. Its low bit is effectively a write-in-progress flag, and its value doubles as a version number: each completed write advances it by two, so any change to the counter between a reader's two observations means at least one write happened in between.
The writer follows a strict three-step ritual. First, increment the counter (even → odd), publishing 'a write is starting'. Second, write the protected data — all the fields of the multi-word struct. Third, increment the counter again (odd → even), publishing 'the write is done and the data is stable'. The two increments bracket the data mutation, so any reader that observes the counter change knows the payload was in flux. Crucially, writers do not get exclusion from the seqlock itself against other writers — the counter protocol only coordinates readers-versus-writer. If two writers can run concurrently they must be serialized by a separate mechanism (write exclusion: a plain spinlock or mutex among writers), or the two-increment invariant breaks and readers can be fooled. In the classic single-writer case (one timer interrupt updating the clock) that lock is unnecessary because there is only ever one writer.
The reader loop is the optimistic core. Read the counter (call it seq_pre); if it is odd, a write is in progress, so spin until it is even before bothering to read the payload. Copy every field of the payload into local variables. Read the counter again (seq_post). If seq_post equals seq_pre (and both even), no write touched the data during the copy — the local snapshot is consistent, and the reader proceeds. If they differ, a write intervened, the copy is potentially torn, and the reader loops from the top. Readers never write shared state, so any number of them run fully in parallel.
The memory ordering is what makes this correct rather than merely plausible. The pre-read of the counter must be an acquire load and the payload reads must not be hoisted above it; the post-read must be an acquire load that is not sunk below the payload reads (an acquire fence between the payload copy and the post-read enforces this). On the writer side, the first increment is a relaxed-or-release store and — critically — the payload writes must not be reordered before it, while the second increment must be a release store so that a reader observing the new even counter is guaranteed to also observe all the payload writes that preceded it. Get the fences wrong and the seqlock silently returns torn data on weakly-ordered CPUs while passing every test on your strongly-ordered laptop. The payload must also be free of anything a reader might act on before validation — no raw pointers to dereference, no lengths to index with — so torn reads stay harmless until the counter check throws them away.
End-to-end flow
Trace a reader racing a writer over a two-word timestamp {seconds, nanos}, updated by a timer and read by everything. Steady state: the counter is 100 (even), seconds=1000, nanos=0.
The uncontended read: a reader loads the counter — 100, even — and copies seconds=1000, nanos=0 into locals. It re-loads the counter: still 100. Pre equals post, both even; the snapshot is consistent; the reader returns {1000, 0}. Cost: two counter reads and a two-word copy, no atomic write, no cache-line contention with other readers. A thousand cores doing this simultaneously all hold the counter's cache line shared and never contend.
The read that races a write: a reader loads the counter — 100 — and begins copying. It reads seconds=1000. Meanwhile the timer fires: the writer increments the counter to 101 (odd), writes seconds=1001, writes nanos=0, and increments the counter to 102 (even). Our reader, unaware, reads nanos — and depending on exact timing it may read the old 0 or the new 0. It now holds a snapshot that could be torn: seconds=1000 (old) paired with whatever nanos it caught. It re-loads the counter: 102, not 100. Pre (100) ≠ post (102), so a write happened during the copy; the snapshot is discarded. The reader loops, reads the counter (102, even), copies seconds=1001, nanos=0, re-checks the counter (102), matches, and returns the correct, consistent {1001, 0}. The torn intermediate never escaped — it was detected and thrown away.
The read that starts mid-write: a reader loads the counter and finds 101 (odd) — a write is in progress right now. Rather than copy fields that are actively being mutated, it spins on the counter until it goes even (102), then proceeds with the normal read-copy-recheck. This early odd-check is an optimization: it avoids a guaranteed-doomed copy when the reader can already see a write is underway.
Why the fences matter, concretely: suppose on a weakly-ordered CPU the reader's payload read of seconds were speculatively reordered to after the post-counter check. The reader could check the counter (102, matches its stale pre-value in some interleaving), conclude the read is valid, and only then load seconds — picking up a value from a write that its counter check never covered. The acquire semantics on the counter loads and the release on the writer's final increment are precisely what forbid this reordering: they establish that observing the even counter implies observing all prior payload writes, and that the payload reads are pinned between the two counter observations. Remove them and the protocol is a very fast way to read corrupt data on exactly the hardware you deployed to but never tested on.