Reaction: "A Quick Look at Atomics in C"
When a Single Line of Code Hides an Abyss
There are topics in programming that leave a mark on you. Not because they are spectacular, but because they make you realize, often too late, that you have been writing fragile code for years without knowing it. Atomics in C are one of those topics. The first time I truly understood what happens under the hood of a simple x++, I felt something strange, a mix of vertigo and humility. The ground shifted beneath the feet of a developer who had been far too confident.
The source article, drawn from point 41 on lemire.me, tackles this subject with the sobriety you expect from a technical text. But behind that sobriety hides a brutal truth: in C, almost nothing is atomic by default. And that truth, if you refuse to face it, always ends up catching you.
What the Article Is Really Saying
The starting idea is disarmingly simple: an operation that, when written, looks like a single instruction, can actually break down into several steps at the machine level. An increment, a read, a write, a test: all of this can be interrupted, reordered, observed halfway through. Between the moment the CPU reads the value and the moment it writes it back, another thread can slip in. This is what we call a race condition, and it is the kind of bug that never warns you in advance.
The C11 standard introduced <stdatomic.h> to answer this problem. The _Atomic types, the atomic_load, atomic_store, atomic_fetch_add operations — all of this exists to guarantee one thing: that an operation happens in an indivisible way. No visible intermediate state. No wild reordering by the compiler. No surprise from the CPU side.
But the article does not stop there, and that is what makes it interesting. It reminds us that atomics are not just a matter of "lightweight locking." Every atomic operation carries with it a memory semantics. memory_order_relaxed, acquire, release, acq_rel, seq_cst: these words are not ornaments. They define a precise contract between threads, between what one thread writes and what another can legitimately observe. Choosing the wrong order means either wasting performance or silently reintroducing the bug you thought you had eliminated.
What I Feel When Reading This
Honestly, this kind of article gives me a form of respect mixed with concern. Respect, because it takes courage to write correct concurrent code. Concern, because I know how easy it is to believe you are safe when you are not.
I have seen, in my career, concurrency bugs that cost weeks of investigation. Bugs that disappeared when you added a printf. Bugs that only reproduced on one machine out of fifty, only under heavy load, only on a Friday afternoon when the moon was aligned with Jupiter. Those bugs do not come from nowhere. They almost always come from an unverified assumption about the atomicity of an operation.
What strikes me is that this subject is intimidating but accessible. Accessible, because the tools exist, they are standardized, documented. Intimidating, because the gap between "I know _Atomic" and "I know when and how to use it correctly" is much wider than it looks.
A Professional Explanation, Without Detours
Let me explain concretely, the way I would to a junior colleague, why all of this matters.
Imagine two threads sharing a counter. Thread A does counter++. Thread B does the same. Naively, you tell yourself the result will be +2. In reality, here is what can happen at the machine level: thread A reads the value (let's say 5). Thread B reads the value (still 5). Thread A writes 6. Thread B writes 6. Result: 6 instead of 7. One increment has been lost. And this is not a theoretical case, it is a case that happens in production, on servers, in banking systems, in game engines, in kernels.
What atomics bring is a guarantee that this read-modify-write sequence happens as one indivisible block. No other thread can observe the intermediate state. No increment can be lost.
But here is where it gets subtle. If you use atomic_fetch_add with memory_order_relaxed, you guarantee the atomicity of the operation itself, but not the order of the other memory accesses around it. That means another thread might see your counter updated, but not the data you wrote just before. This is where the concept of a memory barrier comes in. The acquire and release orders create synchronization relationships between threads. seq_cst, the default order, guarantees global consistency, at the cost of a higher price.
My Opinion, Unfiltered
My opinion fits in a few sentences. First: if you are not absolutely certain of the memory semantics you need, stay in seq_cst. It is slower, yes. But it is correct. And a correct, slightly slow program is infinitely better than a fast program that corrupts its data once in a thousand runs.
Second: avoid sharing data between threads when you can avoid it. It is the best concurrency optimization that exists. No sharing, no race. It sounds almost philosophical, but it is true. The best concurrent systems I have seen were the ones with the fewest synchronization points, not the ones using the most exotic atomics.
Third: document your reasoning. A memory_order_relaxed without a comment is a time bomb for whoever reads your code in two years, even if that person turns out to be you. Writing why you chose a particular order is often worth more than the choice itself.
What I sometimes criticize about this kind of article is precisely that it stops at the overview. An overview is fine to spark curiosity. But a developer who walks away with an overview and believes himself armed is a dangerous developer for his team. True mastery of atomics requires months of practice, reading, and mistakes. We need to be honest about that.
Why This Topic Fascinates Low-Level Developers So Much
There is an almost visceral reason for the enthusiasm around atomics in low-level communities. It is a subject where the developer's mental model collides head-on with material reality. We like to believe that code executes line by line, in the order we wrote it. That is comfortable. It is false.
The CPU reorders instructions to optimize. The compiler does the same. The memory cache lies about the real state of the data. The memory model of the language itself is not the one we naively imagine. Atomics are one of the rare tools that force us to look this reality in the face, without a filter.
And then, there is this almost initiatory aspect. There are those who know _Atomic by name. Those who know how to use it without breaking their program. And those who know when not to use it, which is probably the highest level. This gradation naturally creates passionate, sometimes tense, often enriching discussions in systems, kernel, and embedded communities. Because deep down, mastering atomics means mastering the border between the abstraction of the language and the hardness of silicon.
What You Absolutely Need to Remember
Three ideas, if you only have to remember three things.
First: an operation that looks non-atomic can be broken down into several steps. Never assume that one line of C corresponds to one indivisible machine instruction. That assumption is the source of most concurrency bugs I have ever observed.
Second: atomics are not just a matter of "fast locking." It is a memory contract between threads. Misused, an atomic can be as dangerous, or even more dangerous, than a normal variable, because it gives a false sense of security.
Third: simplicity always wins. If you can avoid sharing, avoid it. If you must share, use the standard tools, respect the default orders as long as you do not have a measured reason to relax them, and document every choice. A randomly chosen memory_order is a time bomb that a future version of yourself will have to defuse, probably at three in the morning, in production.
Conclusion
"A Quick Look at Atomics in C" is not a spectacular article. It is a technical reminder, almost austere, without frills. But it is precisely this kind of reminder that separates code that works from code that seems to work. And in a world where concurrency is everywhere, where every CPU core is an invitation to parallelize, ignoring atomics amounts to choosing to write bugs you will never truly understand.
So yes, the subject is technical. Yes, it demands a sincere effort. Yes, you have to accept not mastering everything on the first try. But that is the price to pay for writing C that stands, that survives load, that does not betray its users. And honestly, when you love this language, when you respect what it allows you to build, that price is more than worth the detour.
Because deep down, understanding atomics is not just learning an API. It is learning to think about concurrency. And no shortcut can offer you that.