|
24 | 24 | //! |
25 | 25 | //! ## Memory model for atomic accesses |
26 | 26 | //! |
27 | | -//! Rust atomics currently follow the same rules as [C++20 atomics][cpp], specifically `atomic_ref`. |
28 | | -//! Basically, creating a *shared reference* to one of the Rust atomic types corresponds to creating |
29 | | -//! an `atomic_ref` in C++; the `atomic_ref` is destroyed when the lifetime of the shared reference |
30 | | -//! ends. A Rust atomic type that is exclusively owned or behind a mutable reference does *not* |
31 | | -//! correspond to an “atomic object” in C++, since the underlying primitive can be mutably accessed, |
32 | | -//! for example with `get_mut`, to perform non-atomic operations. |
| 27 | +//! Rust atomics currently follow the same rules as [C++20 atomics][cpp], specifically the rules |
| 28 | +//! from the [`intro.races`][cpp-intro.races] section, without the "consume" memory ordering. Since |
| 29 | +//! C++ uses an object-based memory model whereas Rust is access-based, a bit of translation work |
| 30 | +//! has to be done to apply the C++ rules to Rust: whenever C++ talks about "the value of an |
| 31 | +//! object", we understand that to mean the resulting bytes obtained when doing a read. When the C++ |
| 32 | +//! standard talks about "the value of an atomic object", this refers to the result of doing an |
| 33 | +//! atomic load (via the operations provided in this module). A "modification of an atomic object" |
| 34 | +//! refers to an atomic store. |
| 35 | +//! |
| 36 | +//! The end result is *almost* equivalent to saying that creating a *shared reference* to one of the |
| 37 | +//! Rust atomic types corresponds to creating an `atomic_ref` in C++, with the `atomic_ref` being |
| 38 | +//! destroyed when the lifetime of the shared reference ends. The main difference is that Rust |
| 39 | +//! permits concurrent atomic and non-atomic reads to the same memory as those cause no issue in the |
| 40 | +//! C++ memory model, they are just forbidden in C++ because memory is partitioned into "atomic |
| 41 | +//! objects" and "non-atomic objects" (with `atomic_ref` temporarily converting a non-atomic object |
| 42 | +//! into an atomic object). |
| 43 | +//! |
| 44 | +//! That said, Rust *does* inherit the C++ limitation that non-synchronized atomic accesses may not |
| 45 | +//! partially overlap: they must be either disjoint or access the exact same memory. This in |
| 46 | +//! particular rules out non-synchronized differently-sized accesses to the same data. |
33 | 47 | //! |
34 | 48 | //! [cpp]: https://en.cppreference.com/w/cpp/atomic |
| 49 | +//! [cpp-intro.races]: https://timsong-cpp.github.io/cppwp/n4868/intro.multithread#intro.races |
35 | 50 | //! |
36 | 51 | //! Each method takes an [`Ordering`] which represents the strength of |
37 | | -//! the memory barrier for that operation. These orderings are the |
38 | | -//! same as the [C++20 atomic orderings][1]. For more information see the [nomicon][2]. |
| 52 | +//! the memory barrier for that operation. These orderings behave the |
| 53 | +//! same as the corresponding [C++20 atomic orderings][1]. For more information see the [nomicon][2]. |
39 | 54 | //! |
40 | 55 | //! [1]: https://en.cppreference.com/w/cpp/atomic/memory_order |
41 | 56 | //! [2]: ../../../nomicon/atomics.html |
42 | 57 | //! |
43 | | -//! Since C++ does not support mixing atomic and non-atomic accesses, or non-synchronized |
44 | | -//! different-sized accesses to the same data, Rust does not support those operations either. |
45 | | -//! Note that both of those restrictions only apply if the accesses are non-synchronized. |
46 | | -//! |
47 | 58 | //! ```rust,no_run undefined_behavior |
48 | 59 | //! use std::sync::atomic::{AtomicU16, AtomicU8, Ordering}; |
49 | 60 | //! use std::mem::transmute; |
|
52 | 63 | //! let atomic = AtomicU16::new(0); |
53 | 64 | //! |
54 | 65 | //! thread::scope(|s| { |
55 | | -//! // This is UB: mixing atomic and non-atomic accesses |
56 | | -//! s.spawn(|| atomic.store(1, Ordering::Relaxed)); |
57 | | -//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); |
| 66 | +//! // This is UB: conflicting concurrent accesses. |
| 67 | +//! s.spawn(|| atomic.store(1, Ordering::Relaxed)); // atomic store |
| 68 | +//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); // non-atomic write |
58 | 69 | //! }); |
59 | 70 | //! |
60 | 71 | //! thread::scope(|s| { |
61 | | -//! // This is UB: even reads are not allowed to be mixed |
62 | | -//! s.spawn(|| atomic.load(Ordering::Relaxed)); |
63 | | -//! s.spawn(|| unsafe { atomic.as_ptr().read() }); |
| 72 | +//! // This is fine: the accesses do not conflict (as none of them performs any modification). |
| 73 | +//! // In C++ this would be disallowed since creating an `atomic_ref` precludes |
| 74 | +//! // further non-atomic accesses, but Rust does not have that limitation. |
| 75 | +//! s.spawn(|| atomic.load(Ordering::Relaxed)); // atomic load |
| 76 | +//! s.spawn(|| unsafe { atomic.as_ptr().read() }); // non-atomic read |
64 | 77 | //! }); |
65 | 78 | //! |
66 | 79 | //! thread::scope(|s| { |
67 | 80 | //! // This is fine, `join` synchronizes the code in a way such that atomic |
68 | | -//! // and non-atomic accesses can't happen "at the same time" |
69 | | -//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); |
70 | | -//! handle.join().unwrap(); |
71 | | -//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); |
| 81 | +//! // and non-atomic accesses can't happen "at the same time". |
| 82 | +//! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); // atomic store |
| 83 | +//! handle.join().unwrap(); // synchronize |
| 84 | +//! s.spawn(|| unsafe { atomic.as_ptr().write(2) }); // non-atomic write |
72 | 85 | //! }); |
73 | 86 | //! |
74 | 87 | //! thread::scope(|s| { |
75 | | -//! // This is UB: using different-sized atomic accesses to the same data |
| 88 | +//! // This is UB: using differently-sized atomic accesses to the same data. |
| 89 | +//! // (It would be UB even if these are both loads.) |
76 | 90 | //! s.spawn(|| atomic.store(1, Ordering::Relaxed)); |
77 | 91 | //! s.spawn(|| unsafe { |
78 | 92 | //! let differently_sized = transmute::<&AtomicU16, &AtomicU8>(&atomic); |
|
82 | 96 | //! |
83 | 97 | //! thread::scope(|s| { |
84 | 98 | //! // This is fine, `join` synchronizes the code in a way such that |
85 | | -//! // differently-sized accesses can't happen "at the same time" |
| 99 | +//! // differently-sized accesses can't happen "at the same time". |
86 | 100 | //! let handle = s.spawn(|| atomic.store(1, Ordering::Relaxed)); |
87 | 101 | //! handle.join().unwrap(); |
88 | 102 | //! s.spawn(|| unsafe { |
|
0 commit comments