Concepts / Atomics and Lock-Free Programming

Atomics and Lock-Free Programming

Atomics are the tool that makes lock-free programming possible.

  • Programming

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?

  • Always 2

  • Either 1 or 2, depending on how the operations interleave

Reveal answer
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.

Concurrency toolsHigher level: takesresponsibility awayStandard-libraryconcurrencyfacilitiesLower level: directcontrolMemory modelAtomicsLock-free programming
Which concurrency tools take responsibility away from you (higher level) and which give you direct control (lower level)?

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

Definition

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.

Key point

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.

Thread AShared counterstarts at 0Thread B1. reads 02. reads 03. writes 14. writes 1 (A's update is lost)
In what order could the reads and writes interleave, and where does the unsynchronized access produce an invalid result?
Worked example

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.

  1. 1
    Step 1

    Thread A reads the counter and sees 0. It will compute 0 + 1 = 1 in its own local work.

  2. 2
    Step 2

    Before Thread A writes, Thread B also reads the counter and sees 0, because nothing has been written yet.

  3. 3
    Step 3

    Thread A writes 1 to the counter.

  4. 4
    Step 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.

Result

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.

Read current valueValue changed: retryCompute new valueAttempt the updateUpdate succeededstill matchesanother thread changed itread again
When a thread's update fails because another thread changed the value first, what happens next, and how does the loop repeat until it succeeds?

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.

In the real world

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.

Key point

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 like

    Using atomics directly to build a shared counter when a standard-library facility already handles the update.

    Why it fails

    Each 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 instead

    Start 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 like

    Two threads each read a counter, add 1, and write it back with no coordination, expecting the total to be 2.

    Why it fails

    The threads can interleave their reads and writes, so one update can overwrite the other and the result depends on timing.

    Do this instead

    Coordinate 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 fails

    The memory model exists to let most programmers reason about shared values as they were written, without thinking about the machine architecture.

    Do this instead

    Reason 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

MEDIUM

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.

More concepts starting with A