Atomics and Lock-Free Programming
Atomics are the tool that makes lock-free programming possible.
The Choice Hidden in Every Concurrent Program
When you write a concurrent program, you face a choice that is easy to overlook: how close to the machine should your code get? Concurrency tools exist at many levels. Some hide almost every detail of how threads coordinate and how memory is seen by different parts of a program. Others expose those details so you can control them directly. The guidance for this part of the material is simple to state and hard to follow in practice: work at the highest level of abstraction that you can afford.
What do you think happens?
What do you think the counter ends with: always 2, or could it be different?
Reveal answer
Either 1 or 2, depending on how the operations interleave. The final value is not guaranteed to be 2.
Both threads can read 0 before either one writes. Each then writes 1, so one increment is lost. The next section traces exactly how this happens.
Keep the answer to that question in mind. Coordination mistakes like this one are the reason the guidance points toward higher-level tools, so the rest of this article follows the chain of reasoning: what the higher-level tools give you, what the lower-level tools ask of you, and when the lower-level tools are still a reasonable choice.
Levels of Abstraction in Concurrency
A useful way to think about abstraction is that each level promises to take some responsibility off your shoulders. A higher-level facility makes more decisions for you, which means you have fewer chances to make the subtle mistakes that concurrent code is prone to. A lower-level facility gives you more control, but every decision you take on by hand is a decision you can get wrong. The guideline asks you to accept the help that higher-level tools offer whenever that help is enough.
The diagram sorts the tools into two groups. The higher group contains tools that take responsibility away from you. The lower group contains tools that give you direct control, and that control is exactly what makes them hard to use correctly. Notice that the same logic applies beyond individual facilities. The source suggests that you consider processes as an alternative to threads. The lesson is not that processes are always better. It is that you should look at the alternatives before committing to the level of machinery you first think of.
Standard-Library Facilities and the Memory Model
The standard-library concurrency facilities are the higher-level tools the guideline has in mind. They are type safe, which in practical terms means they are designed so that the compiler can check how they are used. That check catches a class of errors before the program runs. Preferring these facilities is one of the most direct ways to work at a high level of abstraction.
Behind these facilities sits the memory model. Its purpose is to save most programmers from having to think about the machine architecture level of a computer. It also makes memory appear roughly as naively expected. This is why most programmers can reason about shared data in terms of the values they wrote, rather than in terms of processor caches and reordering, which make the underlying hardware more complicated.
The standard-library facilities and the memory model work together. The facilities give you a checked way to express concurrent work, and the memory model gives you a predictable picture of shared memory to reason about. Together they are the sensible default.
Why Data Races Break Shared Data
A data race happens when two threads access the same memory location without coordination and at least one of those accesses is a write. The source material says data races should be avoided, and the reason becomes clear once you trace what the threads actually do. The diagram below shows one possible order of operations for the counter from the opening question. Each thread performs three steps: read the counter, add 1 to the value it read, and write the result back.
Tracing the Lost Increment
Both threads increment a shared counter that starts at 0. Each thread reads, adds 1, and writes. Determine the final value for the interleaving shown in the diagram.
- 1Step 1
Thread A reads the counter and sees 0. It will compute 0 + 1 = 1 in its own local work.
- 2Step 2
Before Thread A writes, Thread B also reads the counter and sees 0, because nothing has been written yet.
- 3Step 3
Thread A writes 1 to the counter.
- 4Step 4
Thread B, working from the 0 it read earlier, also computes 1 and writes 1. The write from Thread A is overwritten, and the second increment is effectively lost.
The final value is 1, not 2. The program's result depends on timing, which is exactly why an unsynchronized access to shared data is an invalid design.
Atomics and the Lock-Free Path
Atomics are the tool that makes lock-free programming possible. The source states that atomics allow for lock-free programming, which means code can coordinate access to shared data without using ordinary locks. That capability is real and sometimes useful, but the source is direct about who should attempt it: lock-free programming should be left to experts.
The reasoning follows from the rest of the guideline. Every level you drop below the standard facilities is a level where you must make decisions that the higher-level tools would otherwise make for you. When the stakes are subtle correctness questions about shared memory, the safer habit is to stay at the level where those decisions are already handled. The source material does not walk through the internal mechanics of atomic operations, so this article does not describe them in detail.
This sketch is a generic illustration of the retry idea that lock-free designs rely on, not a description of any particular atomic instruction. Its lesson is that a lock-free update does not wait behind a lock. Instead, a thread that loses a race goes back and tries again, and the design must be right about every one of those steps. That is the kind of subtle reasoning the source leaves to experts.
When a Sequential Solution Wins
Working at a high level of abstraction also means being willing to decline concurrency altogether. The source states that sometimes a sequential solution is simpler and faster than a concurrent one. This is easy to forget when concurrency feels like the modern, powerful choice. A program with one thread of control has no shared-data coordination to get wrong, and it avoids any cost that threads and synchronization add.
Suppose a program must sum the values in a short list of 50 numbers. A single loop that adds each value to a total runs in one thread, needs no locks, and cannot suffer a data race. Splitting that loop across several threads would add the work of creating threads, dividing the list, and combining partial totals, and the overhead of that coordination can easily outweigh the small amount of arithmetic saved. The sequential version is simpler to write, simpler to check, and in a case like this, likely faster.
Concurrency should be a justified choice, not an automatic one. Before you add threads, ask whether the work is large enough to benefit and whether the coordination it requires is worth the risk.
Mistakes Developers Make with Concurrency Tools
Common mistake Reaching for the lowest-level tool first because it looks most powerful
What it looks likeUsing atomics directly to build a shared counter when a standard-library facility already handles the update.
Why it failsEach level you drop below the standard facilities adds decisions you must get right by hand, and the source reserves lock-free work for experts.
Do this insteadStart with the highest-level facility that meets your need, and move lower only when you can justify it.
Common mistake Assuming an unsynchronized read-then-write will behave like a single step
What it looks likeTwo threads each read a counter, add 1, and write it back with no coordination, expecting the total to be 2.
Why it failsThe threads can interleave their reads and writes, so one update can overwrite the other and the result depends on timing.
Do this insteadCoordinate access to shared data so that the read and the write cannot be interleaved with another thread's conflicting access.
Common mistake Adding threads to a task so small that coordination costs more than the work itself
Common mistake Assuming the memory model requires you to think about processor caches
Why it failsThe memory model exists to let most programmers reason about shared values as they were written, without thinking about the machine architecture.
Do this insteadReason about shared data at the level the standard facilities and memory model expose, unless you have a specific expert-level reason to go lower.
Checking Your Judgment on a Design
A colleague proposes splitting a loop that formats eight short log messages across four threads, arguing that concurrency is always faster. Using the guideline from this article, explain whether you would accept the proposal as written, and identify the first question you would ask before deciding.
Hints
- Ask whether the sequential version is simpler and whether the work is large enough to justify thread creation and coordination.
- Consider whether a higher-level facility or a sequential loop would be the default choice.
Key Takeaways
- Work at the highest level of abstraction you can afford, and treat each step lower as something you must justify.
- Standard-library concurrency facilities are type safe, and the memory model lets most programmers reason about shared data as they wrote it, which makes them the sensible default.
- Data races are avoided because unsynchronized reads and writes from different threads can interleave in ways that produce invalid results, such as a lost update.
- Atomics make lock-free programming possible, but the source reserves that work for experts.
- A sequential solution is sometimes simpler and faster, so concurrency should be a deliberate choice rather than an automatic one.