Atomic operations, confusion, confusion

[Quote from Atomics in Rust, chapter 3]
A happens-before relationship is created between a release fence and an acquire fence if any store after the release fence is observed by any load before the acquire fence.

For example, suppose we have one thread executing a release fence followed by three atomic store operations to different variables, and another thread executing three load operations from those same variables followed by an acquire fence, as follows:

Thread 1 	

fence(Release);
A.store(1, Relaxed);
B.store(2, Relaxed);
C.store(3, Relaxed);

	
Thread 2
A.load(Relaxed);
B.load(Relaxed);
C.load(Relaxed);
fence(Acquire);

In this situation, if any of the load operations on thread 2 loads the value from the corresponding store operation of thread 1, the acquire fence on thread 2 happens-before the release fence on thread 1.

Could somebody please explain in simple words what it means?
Thanks

Where is this from? I'm not convinced that the claim is true.

The example is a bit incomplete, as the happens-before relationship, if it happens, is pretty useless: a happens-before relation ship between the fence(Release) in Thread 1 and fence(Acquire) in Thread 2 would guarantee that the effects of all writes before the fence(Release) on Thread 1 will be visible in Thread 2 after the fence(Acquire).[1] Since there’s nothing before fence(Release) and neither is there anything after fence(Acquire) in this example, the consequences of this happens-before relationship are actually none.


  1. I’m not deeply familiar with atomics, so I’m not entirely certain exactly which kind of operations before fence(Release) on Thread 1 and after fence(Acquire) in Thread 2 are affected here; I’m fairly certain that ordinary (non-atomic) memory operations do count; I’m not completely certain what something like “before fence(Release)” means either, if it’s everything that comes before it in the source code, of if some kind of “causal relationship” is required. ↩︎

1 Like

This is a quote from third chapter of: "Rust Atomics and Locks".

With the above explanation out of the way, let’s do a different example…

Thread 1 	

A.store(1, Relaxed);
B.store(2, Relaxed);
fence(Release);
C.store(3, Relaxed);
D.store(4, Relaxed);

	
Thread 2
C.load(Relaxed);
D.load(Relaxed);
fence(Acquire);
A.load(Relaxed);
B.load(Relaxed);

The intuitive effect of the fence is that if fence(Release); has already happened from the perspective of Thread 2, then Thread 2 will be guaranteed to see all memory effects before this fence, i.e. the stores to A and B are visible to the loads in Thread 2 after the fence(Acquire).

The difficulty is in formalizing what it means that “fence(Release); has already happened from the perspective of Thread 2”; importantly, it could always be the case that - intuitively speaking - Thread 2 reaches fence(Acquire) before Thread 1 reaches fence(Release), in which case you’d expect no guarantees whatsoever about any effects of A.store or B.store being visible.

The fence instruction doesn’t really “do” anything, so what does fence(Release); has already happened from the perspective of Thread 2” mean now!? From the perspective of Thread 2 the fence is completely invisble (as I just mentioned, it doesn’t “do” anything), so how can Thread 2 ever “know” it did already “happen”? The way this is nailed down in this, slightly cumbersome, definition you quote is that other memory effects in the code around the fences are used as an indicator. The fence itself cannot be seen from Thread 2, but modifications of memory that (within Thread 1) happen (logically) after the fence(Release) might be visible. And it is such memory effects that are used as an indicator: if anything that happens after the fence(Release) in Thread 1 is visible to Thread 2 by the time (i.e. before) fence(Acquire) is reached, then we do consider Thread 2 to “know” - so to speak - that fence(Release) has happened, and we do expect all the guarantees about A.store and B.store effects being visible (in Thread 2) after the fence(Acquire) as well.

To recap, in the above example, this means that

If Thread 2 sees the effect of either (and possible, but not necessarily both) C.store or D.store (in the corresponding loads),
then Thread 2 is also guaranteed to see both the effects of A.store and B.store (in the corresponding loads).

1 Like

Basically, the idea is that fence(Release) happens-before A.store(1, Relaxed) happens-before A.load(Relaxed) happens-before fence(Acquire), therefore we can conclude that fence(Release) happens-before fence(Acquire).

That said, I think there's a problem with the argument because it makes the following step:

  • We know that A.store(1, Relaxed) comes before A.load(Relaxed) in the modification order for A,
  • therefore A.store(1, Relaxed) happens-before A.load(Relaxed).

However, I don't see any rules in the C++ documentation for memory orders that would allow you to make this conclusion. That document makes statements of the form "if stuff then A before B in modification order", but it never makes any statements of the form "if A before B in modification order then stuff".

2 Likes

Thanks, that really clarified for me. So basically what in between those fences is guaranteed to happen in the order of the instructions specified.
Thanks once again.

Aha! I found the rule that makes the statement correct.

Fence-fence synchronization

A release fence FA in thread A synchronizes-with an acquire fence FB in thread B, if

  • There exists an atomic object M,
  • There exists an atomic write X (with any memory order) that modifies M in thread A
  • FA is sequenced-before X in thread A
  • There exists an atomic read Y (with any memory order) in thread B
  • Y reads the value written by X (or the value would be written by release sequence headed by X if X were a release operation)
  • Y is sequenced-before FB in thread B

In this case, all non-atomic and relaxed atomic stores that are sequenced-before FA in thread A will happen-before all non-atomic and relaxed atomic loads from the same locations made in thread B after FB.

source

So this is a special property of fences that do not apply to normal acquire/release operations.

6 Likes

Hey! Where did you find that rule?

There's a link to the source at the bottom of the quote block.

3 Likes