My understanding is no, since using a relaxed operation then a fence allows reordering from before the exchange to after the store is visible on other threads, before the fence, which disallows reordering across itself but otherwise doesn't affect operations before or after itself.
For example, if the exchange is an effective mutex unlock, the fence would prevent "time travel" stores from after the fence being made available before (or vice versa), but it wouldn't prevent stores from "inside" the lock being visible before the lock was released, meaning you could get data races on the lock content.
I would also argue no, because fences can synchronize with atomic operations on any memory location, while an atomic operation on some memory location will only synchronize with other atomic operations on the same memory location.
OK, they are not generally equivalent. But how about this specific case in the standard library’s implementation of Arc::try_unwrap?
I have copied the code here:
pub fn try_unwrap(this: Self) -> Result<T, Self> {
if this.inner().strong.compare_exchange(1, 0, Relaxed, Relaxed).is_err() {
return Err(this);
}
acquire!(this.inner().strong);
let this = ManuallyDrop::new(this);
let elem: T = unsafe { ptr::read(&this.ptr.as_ref().data) };
let alloc: A = unsafe { ptr::read(&this.alloc) }; // copy the allocator
// Make a weak pointer to clean up the implicit strong-weak reference
let _weak = Weak { ptr: this.ptr, alloc };
Ok(elem)
}
Why not merge the acquire fence into the compare_exchange call? Like this:
pub fn try_unwrap(this: Self) -> Result<T, Self> {
if this.inner().strong.compare_exchange(1, 0, Acquire, Relaxed).is_err() {
return Err(this);
}
let this = ManuallyDrop::new(this);
let elem: T = unsafe { ptr::read(&this.ptr.as_ref().data) };
let alloc: A = unsafe { ptr::read(&this.alloc) }; // copy the allocator
// Make a weak pointer to clean up the implicit strong-weak reference
let _weak = Weak { ptr: this.ptr, alloc };
Ok(elem)
}
that's because for mutex unlocks you need Release ordering instead of Acquire, Release fences go before the atomic store instead of after -- this makes them work just fine, since the Release fence prevents any stores from before the fence (so all stores while the mutex is locked) from being moved to after the fence, so any stores while the mutex is locked are properly ordered before the unlocking store.
This is similar to loads and Acquire fences except Acquire fences go after the atomic load and they prevent later loads from being moved to before the fence.
basically, fences and relaxed atomics can be used instead of acquire and/or release atomics, though they are more powerful than non-relaxed atomics and may be more expensive.
That makes sense - though I don't know how confident I would be without an expert looking at all the combinations even in the obvious usage pattern:
thread A thread B
acquire |
| acquire?
fence |
| or here?
| fence?
scary stuff |
| or here?
fence
release
There are a lot more potential orderings of thread B accesses against thread A! Since the scary stuff can't get past fences on either side and the relaxed exchanges are still atomic, it seems pretty reasonable, but threading and reasonable are not good bedfellows!
Fences generally synchronize "through" an atomic operation. Basically, acquire fences upgrade previous atomic reads from relaxed to acquire, and release fences upgrade future atomic writes from relaxed to release. This is with the exception that the data that normally becomes visible before/after the atomic operation becomes visible before/after the fence instead when upgrades are involved.