atomics: allow atomic and non-atomic reads to race

This commit is contained in:
Ralf Jung 2024-08-07 14:30:26 +02:00
parent 851f698682
commit 76bce58b7a

View File

@ -24,26 +24,37 @@
//! //!
//! ## Memory model for atomic accesses //! ## Memory model for atomic accesses
//! //!
//! Rust atomics currently follow the same rules as [C++20 atomics][cpp], specifically `atomic_ref`. //! Rust atomics currently follow the same rules as [C++20 atomics][cpp], specifically the rules
//! Basically, creating a *shared reference* to one of the Rust atomic types corresponds to creating //! from the [`intro.races`][cpp-intro.races] section, without the "consume" memory ordering. Since
//! an `atomic_ref` in C++; the `atomic_ref` is destroyed when the lifetime of the shared reference //! C++ uses an object-based memory model whereas Rust is access-based, a bit of translation work
//! ends. A Rust atomic type that is exclusively owned or behind a mutable reference does *not* //! has to be done to apply the C++ rules to Rust: whenever C++ talks about "the value of an
//! correspond to an “atomic object” in C++, since the underlying primitive can be mutably accessed, //! object", we understand that to mean the resulting bytes obtained when doing a read. When the C++
//! for example with `get_mut`, to perform non-atomic operations. //! standard talks about "the value of an atomic object", this refers to the result of doing an
//! atomic load (via the operations provided in this module). A "modification of an atomic object"
//! refers to an atomic store.
//!
//! The end result is *almost* equivalent to saying that creating a *shared reference* to one of the
//! Rust atomic types corresponds to creating an `atomic_ref` in C++, with the `atomic_ref` being
//! destroyed when the lifetime of the shared reference ends. The main difference is that Rust
//! permits concurrent atomic and non-atomic reads to the same memory as those cause no issue in the
//! C++ memory model, they are just forbidden in C++ because memory is partitioned into "atomic
//! objects" and "non-atomic objects" (with `atomic_ref` temporarily converting a non-atomic object
//! into an atomic object).
//!
//! That said, Rust *does* inherit the C++ limitation that non-synchronized atomic accesses may not
//! partially overlap: they must be either disjoint or access the exact same memory. This in
//! particular rules out non-synchronized differently-sized accesses to the same data.
//! //!
//! [cpp]: https://en.cppreference.com/w/cpp/atomic //! [cpp]: https://en.cppreference.com/w/cpp/atomic
//! [cpp-intro.races]: https://timsong-cpp.github.io/cppwp/n4868/intro.multithread#intro.races
//! //!
//! Each method takes an [`Ordering`] which represents the strength of //! Each method takes an [`Ordering`] which represents the strength of
//! the memory barrier for that operation. These orderings are the //! the memory barrier for that operation. These orderings behave the
//! same as the [C++20 atomic orderings][1]. For more information see the [nomicon][2]. //! same as the corresponding [C++20 atomic orderings][1]. For more information see the [nomicon][2].
//! //!
//! [1]: https://en.cppreference.com/w/cpp/atomic/memory_order //! [1]: https://en.cppreference.com/w/cpp/atomic/memory_order
//! [2]: ../../../nomicon/atomics.html //! [2]: ../../../nomicon/atomics.html
//! //!
//! Since C++ does not support mixing atomic and non-atomic accesses, or non-synchronized
//! different-sized accesses to the same data, Rust does not support those operations either.
//! Note that both of those restrictions only apply if the accesses are non-synchronized.
//!
//! ```rust,no_run undefined_behavior //! ```rust,no_run undefined_behavior
//! use std::sync::atomic::{AtomicU16, AtomicU8, Ordering}; //! use std::sync::atomic::{AtomicU16, AtomicU8, Ordering};
//! use std::mem::transmute; //! use std::mem::transmute;
@ -52,27 +63,30 @@
//! let atomic = AtomicU16::new(0); //! let atomic = AtomicU16::new(0);
//! //!
//! thread::scope(|s| { //! thread::scope(|s| {
//! // This is UB: mixing atomic and non-atomic accesses //! // This is UB: conflicting concurrent accesses.
//! s.spawn(|| atomic.store(1, Ordering::Relaxed)); //! s.spawn(|| atomic.store(1, Ordering::Relaxed)); // atomic store
//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); //! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); // non-atomic write
//! }); //! });
//! //!
//! thread::scope(|s| { //! thread::scope(|s| {
//! // This is UB: even reads are not allowed to be mixed //! // This is fine: the accesses do not conflict (as none of them performs any modification).
//! s.spawn(|| atomic.load(Ordering::Relaxed)); //! // In C++ this would be disallowed since creating an `atomic_ref` precludes
//! s.spawn(|| unsafe { atomic.as_ptr().read() }); //! // further non-atomic accesses, but Rust does not have that limitation.
//! s.spawn(|| atomic.load(Ordering::Relaxed)); // atomic load
//! s.spawn(|| unsafe { atomic.as_ptr().read() }); // non-atomic read
//! }); //! });
//! //!
//! thread::scope(|s| { //! thread::scope(|s| {
//! // This is fine, `join` synchronizes the code in a way such that atomic //! // This is fine, `join` synchronizes the code in a way such that atomic
//! // and non-atomic accesses can't happen "at the same time" //! // and non-atomic accesses can't happen "at the same time".
//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); //! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); // atomic store
//! handle.join().unwrap(); //! handle.join().unwrap(); // synchronize
//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); //! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); // non-atomic write
//! }); //! });
//! //!
//! thread::scope(|s| { //! thread::scope(|s| {
//! // This is UB: using different-sized atomic accesses to the same data //! // This is UB: using differently-sized atomic accesses to the same data.
//! // (It would be UB even if these are both loads.)
//! s.spawn(|| atomic.store(1, Ordering::Relaxed)); //! s.spawn(|| atomic.store(1, Ordering::Relaxed));
//! s.spawn(|| unsafe { //! s.spawn(|| unsafe {
//! let differently_sized = transmute::<&AtomicU16, &AtomicU8>(&atomic); //! let differently_sized = transmute::<&AtomicU16, &AtomicU8>(&atomic);
@ -82,7 +96,7 @@
//! //!
//! thread::scope(|s| { //! thread::scope(|s| {
//! // This is fine, `join` synchronizes the code in a way such that //! // This is fine, `join` synchronizes the code in a way such that
//! // differently-sized accesses can't happen "at the same time" //! // differently-sized accesses can't happen "at the same time".
//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); //! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed));
//! handle.join().unwrap(); //! handle.join().unwrap();
//! s.spawn(|| unsafe { //! s.spawn(|| unsafe {