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 `try_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 try_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        // try_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        //
200        // NOTE: this is implemented as a single atomic step, so `f` must be pure. We can guarantee
201        // that internally, but not for user-provided callbacks. The public `try_update` and
202        // `update` methods run user callbacks, so they loop over `load` and `compare_exchange_weak`
203        // instead, just like `std`.
204        thread::switch();
205        self.exhale_clock(); // for the load()
206        let current = *self.inner.borrow();
207        let ret = if let Some(new) = f(current) {
208            *self.inner.borrow_mut() = new;
209            self.inhale_clock(); // for the store()
210            Ok(current)
211        } else {
212            Err(current)
213        };
214        ret
215    }
216
217    unsafe fn raw_load(&self) -> T {
218        *self.inner.borrow()
219    }
220
221    fn init_clock(&self) {
222        self.clock.borrow_mut().get_or_insert(VectorClock::new());
223    }
224
225    // Increment the clock for the current thread, and update the Atomic's clock with it
226    // The Atomic (self) "inhales" the clock from the thread
227    fn inhale_clock(&self) {
228        self.init_clock();
229        ExecutionState::with(|s| {
230            let clock = s.increment_clock();
231            let mut self_clock = self.clock.borrow_mut();
232            self_clock.as_mut().unwrap().update(clock);
233        });
234    }
235
236    // Increment the clock for the current thread, and update with the Atomic's current clock
237    // The Atomic (self) "exhales" its clock to the thread
238    fn exhale_clock(&self) {
239        self.init_clock();
240        ExecutionState::with(|s| {
241            let self_clock = self.clock.borrow();
242            s.update_clock(self_clock.as_ref().unwrap());
243        });
244    }
245
246    #[cfg(test)]
247    fn signature(&self) -> ResourceSignature {
248        self.signature.clone()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use std::collections::HashSet;
255
256    use crate::sync::atomic::*;
257
258    #[test]
259    fn unique_resource_signatures() {
260        let atomic_i8 = AtomicI8::new(0);
261        let atomic_i16 = AtomicI16::new(0);
262        let atomic_i32 = AtomicI32::new(0);
263        let atomic_i64 = AtomicI64::new(0);
264        let atomic_i128 = AtomicI128::new(0);
265        let atomic_isize = AtomicIsize::new(0);
266        let atomic_u8 = AtomicU8::new(0);
267        let atomic_u16 = AtomicU16::new(0);
268        let atomic_u32 = AtomicU32::new(0);
269        let atomic_u64 = AtomicU64::new(0);
270        let atomic_u128 = AtomicU128::new(0);
271        let atomic_usize = AtomicUsize::new(0);
272        let atomic_bool = AtomicBool::new(false);
273        let atomic_ptr = AtomicPtr::new(std::ptr::null_mut::<i32>());
274
275        // All atomics should have unique signatures
276        let signatures = HashSet::from([
277            atomic_i8.signature(),
278            atomic_i16.signature(),
279            atomic_i32.signature(),
280            atomic_i64.signature(),
281            atomic_i128.signature(),
282            atomic_isize.signature(),
283            atomic_u8.signature(),
284            atomic_u16.signature(),
285            atomic_u32.signature(),
286            atomic_u64.signature(),
287            atomic_u128.signature(),
288            atomic_usize.signature(),
289            atomic_bool.signature(),
290            atomic_ptr.signature(),
291        ]);
292
293        // Check all signatures are unique
294        assert_eq!(signatures.len(), 14);
295    }
296
297    #[test]
298    fn atomic_signatures_consistent_across_shuttle_iterations() {
299        use std::sync::{Arc, Mutex};
300
301        let all_signatures = Arc::new(Mutex::new(HashSet::new()));
302        let all_signatures_clone = all_signatures.clone();
303
304        shuttle_schedulers::check_random(
305            move || {
306                let atomic1 = AtomicBool::new(false);
307                let atomic2 = AtomicBool::new(true);
308
309                all_signatures_clone
310                    .lock()
311                    .unwrap()
312                    .insert((atomic1.load(Ordering::SeqCst), atomic1.signature()));
313                all_signatures_clone
314                    .lock()
315                    .unwrap()
316                    .insert((atomic2.load(Ordering::SeqCst), atomic2.signature()));
317            },
318            10,
319        );
320
321        // Should have exactly 2 unique (signatures X values) across all iterations
322        assert_eq!(all_signatures.lock().unwrap().len(), 2);
323    }
324}