Is the acquire load on top necessary in this C11 Chase-Lev Deque implementation?

2026-07-14

Stack Overflow: View Question

Tags: c, multithreading, threadpool, atomic, deque

Score: 4 | Views: 127

The asker is dissecting the C11 adaptation of the Chase-Lev work-stealing deque from Lê et al.'s "Correct and Efficient Work-Stealing for Weak Memory Models." The paper claims the implementation is optimal: no C11 synchronization can be weakened without breaking correctness. The asker isn't buying it — specifically, they can't see why the initial load of top in push() needs memory_order_acquire rather than memory_order_relaxed.

Recall the shape of push():

void push(Deque *q, void *w) {
    size_t b = atomic_load_explicit(&q->bottom, memory_order_relaxed);
    size_t t = atomic_load_explicit(&q->top,    memory_order_acquire); // <-- why acquire?
    Array *a = atomic_load_explicit(&q->array,  memory_order_relaxed);
    if (b - t > a->size - 1) { /* grow */ }
    ...
}

Why it's subtle. Only the owner thread calls push() and take(); stealers race on top. Naively you'd think the owner never reads anything published by stealers — stealers only CAS top forward. So what could an acquire load possibly synchronize with?

The real answer lies in the resize path. The t value determines whether we grow the array. If the load of top is stale, we might grow unnecessarily — annoying, but not incorrect. However, the tricky case is around the interaction with a preceding take(): after a failed CAS in take(), the owner may have written bottom back, and the value of top observed by that CAS establishes a happens-before edge. If the subsequent push() reads a value of top that's older than what the same thread already observed via the CAS in take(), we can violate the invariant bottom ≥ top that the size calculation depends on — leading to a bogus size and either a spurious resize or, worse, an underflow in b - t (unsigned wraparound producing a huge apparent size).

Sketch of a proof direction. The paper's formal model (they mechanized it in Coq/Herd) tracks the "modification order" of top. Relaxed loads by the owner can observe any prior write in that modification order, including ones already superseded by writes the same thread has causally observed. The acquire ensures the owner sees a value at least as new as any it previously read-modify-wrote, closing that window.

Gotchas:

The challenge: Proving that a memory ordering is necessary (not just sufficient) requires constructing a concrete weak-memory execution where relaxing it produces a buggy interleaving — a much harder exercise than the usual "add barriers until it works."

All newsletters