Skip to content

atomic::Ordering docs provide fewer guarantees than C++20 reference #104814

Open
@JanBeh

Description

@JanBeh

Problem description

The documentation in std::sync::atomic and std::sync::atomic::Ordering (source in Rust 1.65) describe that the Orderings are equal to those defined in C++20:

In std::sync::atomic:

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 Orderings. 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 an Acquire (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 Orderings 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 the non_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-docsArea: Documentation for any part of the project, including the compiler, standard library, and tools

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions