|
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