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}