liveBook · Manning

Lockless technique

As explained in C++ Concurrency in Action, Second Edition

A progress guarantee under which, when threads continue taking steps, at least one operation completes despite contention.

A lockless, or lock-free, technique lets concurrent threads make progress without waiting for one particular thread to finish. Its guarantee is collective: when threads keep taking steps, at least one operation completes within a bounded number of steps. A thread suspended midway through an operation must not prevent the others from completing theirs. section 7.1.1 section 7.1.2

Related guarantees answer different questions:

  • Nonblocking means no blocking call is made; it does not establish lock-free progress. A spin lock loops on an atomic flag but still permits only one thread to hold its mutex. section 7.1.1
  • Obstruction-free means a thread completes in bounded steps if every other thread is paused. section 7.1.1
  • Wait-free means every thread completes in bounded steps despite other threads’ activity. Lock-free alone permits one thread to starve while others succeed. section 7.1.1 section 7.1.2

A common pattern reads shared state, prepares a change, then uses compare/exchange to publish it. If another thread changed the state first, the operation retries. Those retries can support lock-free progress because a failed attempt can reflect another thread’s successful change; merely looping on an atomic operation does not make an algorithm lock-free. Check the atomic type’s implementation too: `is_lock_free()` detects an internal lock, while C++17’s `is_always_lock_free` reports a compile-time guarantee across supported target hardware. `std::atomic_flag` is guaranteed lock-free, but building a spin-lock mutex from it does not make the protected data structure lock-free. section 7.1.2 section 5.2.1 section 5.2.2 section 7.1.1

Listing 7.2. Implementing push() without locks
template<typename T>
class lock_free_stack
{
private:
    struct node
    {
        T data;
        node* next;
        node(T const& data_):                                          #1
            data(data_)
        {}
    };
    std::atomic<node*> head;
public:
    void push(T const& data)
    {
        node* const new_node=new node(data);                           #2
        new_node->next=head.load();                                    #3
        while(!head.compare_exchange_weak(new_node->next,new_node));   #4
    }
};


!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":8,\"ch\":71},{\"line\":8,\"ch\":73}],[{\"line\":16,\"ch\":71},{\"line\":16,\"ch\":73}],[{\"line\":17,\"ch\":71},{\"line\":17,\"ch\":73}],[{\"line\":18,\"ch\":71},{\"line\":18,\"ch\":73}]]"}
!@%STYLE%@!

In this stack push, `new_node->next` first records the observed head. `compare_exchange_weak` attempts to replace that head with the new node; on failure, it updates the expected head and the loop retries. The stack therefore need not hold a mutex while another thread pushes. section 7.2.1

Removing a node is harder. Two threads can read the same head; if one removes and deletes it before the other reads its `next` pointer, the second dereferences a dangling pointer. Compare/exchange prevents both threads from claiming the same node, but does not by itself solve reclamation. An initial stack version avoids deletion and consequently leaks nodes. Lockless design must preserve data-structure invariants through indivisible changes as well as satisfy its progress guarantee. section 7.2.1 section 3.1.2

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Defines progress guarantees and explains compare/exchange retries, then exposes unresolved reclamation in an initial stack.
Examples
Contrasts an atomic-flag spin-lock mutex with a compare/exchange stack whose initial pop leaks nodes.
What it assumes
Assumes familiarity with atomic operations and memory ordering for designing lock-free data structures; defines the competing progress guarantees.
What you can do after
Evaluate/understand it by checking whether concurrent operations progress and spotting starvation, hidden locks, and unsafe stack-node deletion.
How it connects
Connects atomic operations and compare/exchange to spin locks, wait-free algorithms, data-structure invariants, and memory reclamation.

Also explained in

Compare how 3 books explain lockless technique →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.