Relationship between C11 atomics and sequence points

164 Views Asked by At

I basically have the following code snippet:

size_t counter = atomic_fetch_sub_explicit(&atomicCounter, 1, memory_order_release);
if (counter - 1 == 0
    && atomic_load_explicit(&anotherAtomicCounter, 1, memory_order_relaxed) == 0 {
      //Some code
}

For correctness, it is important that the atomic load of anotherAtomicCounter occurs after the fetch-and-sub (FAS) of atomicCounter. With the given memory orders, this would normally not be guaranteed and the load could happen before the FAS. However, I was wondering how sequence points have an affect on this particular code. The standard mentions that

If evaluation A is sequenced before evaluation B, then evaluation of A will be complete before evaluation of B begins.

Combined with rule number 2

There is a sequence point after evaluation of the first (left) operand and before evaluation of the second (right) operand of the following binary operators: && (logical AND), || (logical OR), and , (comma).

this means that the atomic load must happen after the comparison but the comparison can only be completed once the result of the FAS is known.

My question is whether these rules guarantee that the atomic load always happens after the FAS, even when using more relaxed memory orders?

Thanks in advance!

2

There are 2 best solutions below

2
orlp On BEST ANSWER

To answer the question in your title, there is no real relationship between atomics and sequence points.

The code as written does guarantee that the compiler must execute the atomic_fetch_sub before the atomic_load. But these functions are (in C's memory model) simply requests to the platform to perform certain actions on certain pieces of memory. Their effects, when they become visible to who, are specified by the memory model, and the ordering parameters. Thus even in the case when you know request A comes before request B, that does not mean the effects of request A are resolved before request B, unless you explicitly specify it to be.

0
Nate Eldredge On

I would say there is a relationship, but it's indirect.

Sequence points are how the language defines the sequenced before partial ordering on evaluations (C17 5.1.2.3), which is basically what you might generically call "program order". It's the order that operations naively appear to be executed in, and for a single-threaded program, it completely determines how those operations interact with each other. It promises, for instance, that if you call foo() && bar(), that bar will see all the updates to global variables that were made by foo; the evaluation of foo() will be complete before the evaluation of bar() begins.

Now the whole point of memory ordering is that the order in which loads and stores are seen by other threads does not necessarily agree with program order. If you write

res = atomic_load_explicit(&a, memory_order_relaxed) || atomic_load_explicit(&b, memory_order_relaxed);

then the load of a is sequenced before the load of b, thanks to the sequence point provided by ||. But because the memory order is relaxed, that is no guarantee that other threads see those operations in the same order. If we previously had a == 0 and b == 1, and another thread does atomic_store(&a, 1); atomic_store(&b, 0); (with seq_cst order, let's say) then it is entirely possible that res ends up with the value 0.

That's the same as the situation with your example code. You have a sequence point between the fetch_sub and the load. (Actually two of them: one at the end of the full expression that initializes counter, and the other at the && which you pointed out.) So the fetch_sub is most definitely sequenced before the load. But your memory ordering isn't strong enough to promise that inter-thread visibility matches sequencing.

However, memory barriers can give you more certainty, and what they do is to ensure that, to some specified extent, the order of visibility to outside observers does agree with program order. If you write

res = atomic_load_explicit(&a, memory_order_seq_cst) || atomic_load_explicit(&b, memory_order_seq_cst);

then the sequence point again guarantees that the load of a is sequenced before the load of b, and now the seq_cst guarantees that other threads see those operations in that order too. Now res will always be set to 1 if another thread does atomic_store(&a, 1); atomic_store(&b, 0);.

But memory barriers only impose, at best, as much ordering as the sequencing already provided. If you instead did

res = atomic_load_explicit(&a, memory_order_seq_cst) + atomic_load_explicit(&b, memory_order_seq_cst);

now the barriers do not help you. There is no sequence point between the two loads, and therefore no guarantees whatsoever about the order in which they are even evaluated, much less about the order in which they become visible.

Formally, the sequenced before relation is used in defining the happens before relation of 5.1.2.4, which is the main tool in determining how loads and stores from different threads affect each other, and in particular, in determining when they do and don't cause data races.