Skip to main content

shuttle_std/sync/atomic/
mod.rs

1//! Atomic types
2//!
3//! Atomic types provide primitive shared-memory communication between threads, and are the building
4//! blocks of other concurrent types.
5//!
6//! This module defines atomic versions of a select number of primitive types, the same as
7//! [`std::sync::atomic`] in the standard library. See that module's documentation for more details.
8//!
9//! # Warning about relaxed behaviors
10//!
11//! Shuttle does not faithfully model behaviors of relaxed atomic orderings (those other than
12//! [`SeqCst`](Ordering::SeqCst)). Code that uses these orderings may contain bugs that Shuttle is
13//! unable to find if the bug requires the relaxed behavior to occur. **Shuttle models *all* atomic
14//! operations as if they were using SeqCst ordering.**
15//!
16//! For example, consider this test that uses a `flag` variable to indicate that data is present in
17//! a separate `data` variable:
18//! ```
19//! # use std::sync::Arc;
20//! # use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21//! # use std::thread;
22//! let flag = Arc::new(AtomicBool::new(false));
23//! let data = Arc::new(AtomicU64::new(0));
24//!
25//! {
26//!     let flag = Arc::clone(&flag);
27//!     let data = Arc::clone(&data);
28//!     thread::spawn(move|| {
29//!         data.store(42, Ordering::Relaxed);
30//!         flag.store(true, Ordering::Relaxed);
31//!     });
32//! }
33//!
34//! if flag.load(Ordering::Relaxed) {
35//!     assert_eq!(data.load(Ordering::Relaxed), 42);
36//! }
37//! ```
38//! This code is incorrect because of the relaxed orderings used for the loads and stores. Some
39//! architectures will allow an execution where `flag` is true but the assertion is false. However,
40//! Shuttle treats all atomic operations as SeqCst, and this test would be correct if we used SeqCst
41//! for all atomic operations, so Shuttle cannot find this bug.
42//!
43//! If you are writing code that relies on relaxed atomic operations and need to check its
44//! correctness, the [Loom] crate provides support for reasoning about Acquire and Release orderings
45//! and partial support for Relaxed orderings.
46//!
47//! To disable the warning printed about this issue, set the `SHUTTLE_SILENCE_WARNINGS` environment
48//! variable to any value, or set the [`silence_warnings`](shuttle_engine::Config::silence_warnings) field of
49//! [`Config`](shuttle_engine::Config) to true.
50//!
51//! [Loom]: https://crates.io/crates/loom
52
53mod bool;
54mod int;
55mod ptr;
56
57pub use self::bool::AtomicBool;
58pub use int::*;
59pub use ptr::AtomicPtr;
60pub use std::sync::atomic::Ordering;
61
62use crate::sync::{ResourceSignature, ResourceType};
63use shuttle_engine::runtime::execution::ExecutionState;
64use shuttle_engine::runtime::task::clock::VectorClock;
65use shuttle_engine::runtime::thread;
66use shuttle_engine::silence_warnings;
67use std::cell::RefCell;
68use std::panic::RefUnwindSafe;
69
70static PRINTED_ORDERING_WARNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
71
72#[inline]
73fn maybe_warn_about_ordering(order: Ordering) {
74    use owo_colors::OwoColorize;
75
76    #[allow(clippy::collapsible_if)]
77    if order != Ordering::SeqCst {
78        if PRINTED_ORDERING_WARNING
79            .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
80            .is_ok()
81        {
82            if silence_warnings() {
83                return;
84            }
85
86            if ExecutionState::with(|state| state.config.silence_warnings) {
87                return;
88            }
89
90            eprintln!(
91                "{}: Shuttle only correctly models SeqCst atomics and treats all other Orderings \
92                as if they were SeqCst. Bugs caused by weaker orderings like {:?} may be missed. \
93                See https://docs.rs/shuttle/*/shuttle/sync/atomic/index.html#warning-about-relaxed-behaviors \
94                for details or to disable this warning.",
95                "WARNING".yellow(),
96                order
97            );
98        }
99    }
100}
101
102/// An atomic fence, like the standard library's [std::sync::atomic::fence].
103pub fn fence(order: Ordering) {
104    if order == Ordering::Relaxed {
105        panic!("there is no such thing as a relaxed fence");
106    }
107
108    maybe_warn_about_ordering(order);
109
110    // SeqCst fences are no-ops in our execution model
111}
112
113// We can just reuse the standard library's compiler fence, as they have no visible run-time
114// behavior and so we need neither insert yieldpoints nor warn about non-SeqCst orderings.
115pub use std::sync::atomic::compiler_fence;
116
117/// Base type for atomic implementations. This type handles generating the right interleavings for
118/// all atomics. The interesting stuff is in `load`, `store`, `swap`, and `fetch_update`; all other
119/// atomic operations are implemented in terms of those four primitives.
120#[derive(Debug)]
121struct Atomic<T> {
122    inner: RefCell<T>,
123    clock: RefCell<Option<VectorClock>>, // wrapped in option to support the const new()
124    #[allow(unused)]
125    signature: ResourceSignature,
126}
127
128// Safety: Atomic is never actually passed across true threads, only across continuations. The
129// RefCell<_> type therefore can't be preempted mid-bookkeeping-operation.
130unsafe impl<T: Sync> Sync for Atomic<T> {}
131impl<T: RefUnwindSafe> RefUnwindSafe for Atomic<T> {}
132
133impl<T> Atomic<T> {
134    #[track_caller]
135    const fn new(v: T) -> Self {
136        // Since this is a `const fn`, the clock associated with this Atomic is assigned a const value of None
137        // (which represents all zeros).  At the time of creation of the atomic, however, we have more causal
138        // knowledge (the value of the current thread's vector clock).  However, it should be safe to initialize
139        // the Atomic's clock to all-zeros because the only way for another thread to access this Atomic is for
140        // a reference to it to be passed to it via some other synchronization mechanism, which will carry the
141        // knowledge about the owning thread's clock.
142        // TODO Check that the argument above is sound
143        Self {
144            inner: RefCell::new(v),
145            clock: RefCell::new(None),
146            signature: ResourceSignature::new_const(ResourceType::Atomic),
147        }
148    }
149}
150
151impl<T: Copy + Eq> Atomic<T> {
152    fn get_mut(&mut self) -> &mut T {
153        self.exhale_clock();
154        self.inner.get_mut()
155    }
156
157    fn into_inner(self) -> T {
158        self.exhale_clock();
159        self.inner.into_inner()
160    }
161
162    fn load(&self, order: Ordering) -> T {
163        maybe_warn_about_ordering(order);
164
165        thread::switch();
166        self.exhale_clock();
167        let value = *self.inner.borrow();
168        value
169    }
170
171    fn store(&self, val: T, order: Ordering) {
172        maybe_warn_about_ordering(order);
173
174        thread::switch();
175        self.inhale_clock();
176        *self.inner.borrow_mut() = val;
177    }
178
179    fn swap(&self, mut val: T, order: Ordering) -> T {
180        maybe_warn_about_ordering(order);
181
182        // swap behaves like { let x = load() ; store(val) ; x }
183        thread::switch();
184        self.exhale_clock(); // for the load
185        self.inhale_clock(); // for the store
186        std::mem::swap(&mut *self.inner.borrow_mut(), &mut val);
187        val
188    }
189
190    fn fetch_update<F>(&self, set_order: Ordering, fetch_order: Ordering, mut f: F) -> Result<T, T>
191    where
192        F: FnMut(T) -> Option<T>,
193    {
194        maybe_warn_about_ordering(set_order);
195        maybe_warn_about_ordering(fetch_order);
196
197        // fetch_update behaves like (ignoring error): { let x = load() ; store(f(x)); x }
198        // in the error case, there is no store, so the register does not inherit the clock of the caller
199        thread::switch();
200        self.exhale_clock(); // for the load()
201        let current = *self.inner.borrow();
202        let ret = if let Some(new) = f(current) {
203            *self.inner.borrow_mut() = new;
204            self.inhale_clock(); // for the store()
205            Ok(current)
206        } else {
207            Err(current)
208        };
209        ret
210    }
211
212    unsafe fn raw_load(&self) -> T {
213        *self.inner.borrow()
214    }
215
216    fn init_clock(&self) {
217        self.clock.borrow_mut().get_or_insert(VectorClock::new());
218    }
219
220    // Increment the clock for the current thread, and update the Atomic's clock with it
221    // The Atomic (self) "inhales" the clock from the thread
222    fn inhale_clock(&self) {
223        self.init_clock();
224        ExecutionState::with(|s| {
225            let clock = s.increment_clock();
226            let mut self_clock = self.clock.borrow_mut();
227            self_clock.as_mut().unwrap().update(clock);
228        });
229    }
230
231    // Increment the clock for the current thread, and update with the Atomic's current clock
232    // The Atomic (self) "exhales" its clock to the thread
233    fn exhale_clock(&self) {
234        self.init_clock();
235        ExecutionState::with(|s| {
236            let self_clock = self.clock.borrow();
237            s.update_clock(self_clock.as_ref().unwrap());
238        });
239    }
240
241    #[cfg(test)]
242    fn signature(&self) -> ResourceSignature {
243        self.signature.clone()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use std::collections::HashSet;
250
251    use crate::sync::atomic::*;
252
253    #[test]
254    fn unique_resource_signatures() {
255        let atomic_i8 = AtomicI8::new(0);
256        let atomic_i16 = AtomicI16::new(0);
257        let atomic_i32 = AtomicI32::new(0);
258        let atomic_i64 = AtomicI64::new(0);
259        let atomic_i128 = AtomicI128::new(0);
260        let atomic_isize = AtomicIsize::new(0);
261        let atomic_u8 = AtomicU8::new(0);
262        let atomic_u16 = AtomicU16::new(0);
263        let atomic_u32 = AtomicU32::new(0);
264        let atomic_u64 = AtomicU64::new(0);
265        let atomic_u128 = AtomicU128::new(0);
266        let atomic_usize = AtomicUsize::new(0);
267        let atomic_bool = AtomicBool::new(false);
268        let atomic_ptr = AtomicPtr::new(std::ptr::null_mut::<i32>());
269
270        // All atomics should have unique signatures
271        let signatures = HashSet::from([
272            atomic_i8.signature(),
273            atomic_i16.signature(),
274            atomic_i32.signature(),
275            atomic_i64.signature(),
276            atomic_i128.signature(),
277            atomic_isize.signature(),
278            atomic_u8.signature(),
279            atomic_u16.signature(),
280            atomic_u32.signature(),
281            atomic_u64.signature(),
282            atomic_u128.signature(),
283            atomic_usize.signature(),
284            atomic_bool.signature(),
285            atomic_ptr.signature(),
286        ]);
287
288        // Check all signatures are unique
289        assert_eq!(signatures.len(), 14);
290    }
291
292    #[test]
293    fn atomic_signatures_consistent_across_shuttle_iterations() {
294        use std::sync::{Arc, Mutex};
295
296        let all_signatures = Arc::new(Mutex::new(HashSet::new()));
297        let all_signatures_clone = all_signatures.clone();
298
299        shuttle_schedulers::check_random(
300            move || {
301                let atomic1 = AtomicBool::new(false);
302                let atomic2 = AtomicBool::new(true);
303
304                all_signatures_clone
305                    .lock()
306                    .unwrap()
307                    .insert((atomic1.load(Ordering::SeqCst), atomic1.signature()));
308                all_signatures_clone
309                    .lock()
310                    .unwrap()
311                    .insert((atomic2.load(Ordering::SeqCst), atomic2.signature()));
312            },
313            10,
314        );
315
316        // Should have exactly 2 unique (signatures X values) across all iterations
317        assert_eq!(all_signatures.lock().unwrap().len(), 2);
318    }
319}