Description
Problem description
The documentation in std::sync::atomic
and std::sync::atomic::Ordering
(source in Rust 1.65) describe that the Ordering
s are equal to those defined in C++20:
Each method takes an
Ordering
which represents the strength of the memory barrier for that operation. These orderings are the same as the C++20 atomic orderings. For more information see the nomicon.
In std::sync::atomic::Ordering
:
Rust’s memory orderings are the same as those of C++20.
[…]
Relaxed
: […] Corresponds to memory_order_relaxed in C++20.
Release
: […] Corresponds to memory_order_release in C++20.
Acquire
: Corresponds to memory_order_acquire in C++20.
AcqRel
[…] Corresponds to memory_order_acq_rel in C++20.
SeqCst
[…] Corresponds to memory_order_seq_cst in C++20.
However, at the same time, the documentation of Ordering::Release
and Ordering::Acquire
(source in Rust 1.6.5) lists the guarantees made by these Ordering
s. In particular:
Release
:When coupled with a store, all previous operations become ordered before any load of this value with
Acquire
(or stronger) ordering. In particular, all previous writes become visible to all threads that perform anAcquire
(or stronger) load of this value.
Acquire
:When coupled with a load, if the loaded value was written by a store operation with
Release
(or stronger) ordering, then all subsequent operations become ordered after that store. In particular, all subsequent loads will see data written before the store.
These guarantees seem to be less than what the C++20 standard guarantees. Thus the explanation of these orderings differs from the C++20 standard.
Consider the following example, where a: AtomicUsize
, for example:
One thread does:
a.store(10, Release);
a.fetch_add(1, Relaxed);
A second thread does:
if a.load(Acquire) == 11 {
/* … */
}
Suppose the second thread reads the value 11 written by the fetch_add
operation in the first thread. As this value was not written by a store operation with Release
or stronger ordering (but with Relaxed
ordering), there are no guarantees that thread two will see data written before any store in thread one.
Looking into the C++20 reference, however, reveals a concept named "Release sequences":
After a release operation A is performed on an atomic object M, the longest continuous subsequence of the modification order of M that consists of […] atomic read-modify-write operations made to M by any thread is known as release sequence headed by A.
To my understanding, the fetch_add
in thread one is part of the release sequence headed by the store
.
Looking into the Working Draft N4861 of the C++ Standard (the final revision isn't available for free), we see that on page 1525:
An atomic operation A that performs a release operation on an atomic object M synchronizes with an atomic operation B that performs an acquire operation on M and takes its value from any side effect in the release sequence headed by A.
Thus a.store
in thread one would synchronize with the a.load
in thread two, even though the rules described in Rust's documentation of std::sync::atomic::Ordering
do not allow this reasoning; i.e. following solely the rules in Rust's documenation, one would (wrongly) assume that there is no synchronization between those two threads if thread two reads the value of 11
written by the fetch_add
operation.
Additional issues with accessibility
Overall, it's very difficult for someone who starts with the Rust documentation to get an overview on what the Rust atomics really do. The C++20 standard isn't available for free and the linked C++ reference (C++20 atomic orderings) uses but does not define the "synchronizes-with" relationship (which is vital for following/understanding the atomic orderings).
Intent of the Rust documentation
Before making any fixes to the documentation, it should be clarified whether the difference between the Rust documentation and the C++20 reference exists intentionally, i.e. is Rust deliberately giving less guarantees to the programmer than the C++20 standard does? (Even though still following the C++20 memory model in practice "as of now".) Or is the Rust documentation incomplete here?
Possible improvements
Understanding atomics seems to be very complex, and it might be difficult to fully cover all details in the documentation of std
. However, the description of the Ordering
s in Rust's documentation should not differ from those given in the C++ reference.
Possible solutions could be:
- Explicitly state that the guarantees listed under each enum variant of
std::sync::atomic::Ordering
are non-exhaustive (not to be confused with thenon_exhaustive
property of the enum itself) and that the C++20 standard is the normative source. - Correctly explain the behavior in regard to release sequences.