cubecl_environment/sync/base.rs
1#[cfg(not(feature = "std"))]
2use spin::{Mutex as MutexImported, MutexGuard, Once as OnceImported, RwLock as RwLockImported};
3#[cfg(feature = "std")]
4use std::sync::{
5 Mutex as MutexImported, MutexGuard, OnceLock as OnceImported, RwLock as RwLockImported,
6};
7
8#[cfg(not(feature = "std"))]
9pub use spin::{Lazy, RwLockReadGuard, RwLockWriteGuard};
10#[cfg(feature = "std")]
11pub use std::sync::{LazyLock as Lazy, RwLockReadGuard, RwLockWriteGuard};
12
13/// A spin-based one-time initialization cell, identical on every target.
14///
15/// Prefer [`SyncOnceCell`] for plain lazy initialization; use this when the
16/// fallible [`spin::Once::try_call_once`] API is needed.
17pub use spin::Once;
18
19#[cfg(target_has_atomic = "ptr")]
20pub use alloc::sync::Arc;
21#[cfg(not(target_has_atomic = "ptr"))]
22pub use portable_atomic_util::Arc;
23
24#[cfg(target_has_atomic = "ptr")]
25pub use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicUsize, Ordering};
26#[cfg(not(target_has_atomic = "ptr"))]
27pub use portable_atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicUsize, Ordering};
28
29// No `AtomicU64`: `target_has_atomic = "ptr"` says nothing about 64-bit
30// atomics, and a 32-bit target can have pointer-width CAS without them
31// (thumbv7m). Its one user is behind `stream_local`, which is `std`, and every
32// std target has them — so it takes `core`'s directly rather than making this
33// shim carry a width it cannot provide on both arms.
34
35/// A mutual exclusion primitive useful for protecting shared data
36///
37/// This mutex will block threads waiting for the lock to become available. The
38/// mutex can also be statically initialized or created via a [`Mutex::new`]
39///
40/// [Mutex] wrapper to make `spin::Mutex` API compatible with `std::sync::Mutex` to swap
41#[derive(Debug, Default)]
42pub struct Mutex<T> {
43 inner: MutexImported<T>,
44}
45
46impl<T> Mutex<T> {
47 /// Creates a new mutex in an unlocked state ready for use.
48 #[inline(always)]
49 pub const fn new(value: T) -> Self {
50 Self {
51 inner: MutexImported::new(value),
52 }
53 }
54
55 /// Locks the mutex blocking the current thread until it is able to do so.
56 ///
57 /// Locking cannot fail, so no `Result` is returned. A poisoned lock is
58 /// recovered rather than reported: a panic under a guard must not turn
59 /// every later lock into a panic, matching the unwind behavior of the spin
60 /// implementation used off-std.
61 #[inline(always)]
62 pub fn lock(&self) -> MutexGuard<'_, T> {
63 #[cfg(not(feature = "std"))]
64 {
65 self.inner.lock()
66 }
67
68 #[cfg(feature = "std")]
69 {
70 self.inner
71 .lock()
72 .unwrap_or_else(|poisoned| poisoned.into_inner())
73 }
74 }
75}
76
77/// A reader-writer lock which is exclusively locked for writing or shared for reading.
78/// This reader-writer lock will block threads waiting for the lock to become available.
79/// The lock can also be statically initialized or created via a [`RwLock::new`]
80/// [`RwLock`] wrapper to make `spin::RwLock` API compatible with `std::sync::RwLock` to swap
81#[derive(Debug)]
82pub struct RwLock<T> {
83 inner: RwLockImported<T>,
84}
85
86impl<T> RwLock<T> {
87 /// Creates a new reader-writer lock in an unlocked state ready for use.
88 #[inline(always)]
89 pub const fn new(value: T) -> Self {
90 Self {
91 inner: RwLockImported::new(value),
92 }
93 }
94
95 /// Locks this rwlock with shared read access, blocking the current thread
96 /// until it can be acquired.
97 ///
98 /// Poisoning is recovered, never reported; see [`Mutex::lock`].
99 #[inline(always)]
100 pub fn read(&self) -> RwLockReadGuard<'_, T> {
101 #[cfg(not(feature = "std"))]
102 {
103 self.inner.read()
104 }
105 #[cfg(feature = "std")]
106 {
107 self.inner
108 .read()
109 .unwrap_or_else(|poisoned| poisoned.into_inner())
110 }
111 }
112
113 /// Locks this rwlock with exclusive write access, blocking the current thread
114 /// until it can be acquired.
115 ///
116 /// Poisoning is recovered, never reported; see [`Mutex::lock`].
117 #[inline(always)]
118 pub fn write(&self) -> RwLockWriteGuard<'_, T> {
119 #[cfg(not(feature = "std"))]
120 {
121 self.inner.write()
122 }
123
124 #[cfg(feature = "std")]
125 {
126 self.inner
127 .write()
128 .unwrap_or_else(|poisoned| poisoned.into_inner())
129 }
130 }
131}
132
133/// An opaque thread identifier.
134///
135/// This is a stub when no std is available to swap with `std::thread::ThreadId`.
136/// There is no way to obtain one on such targets, so it exists only to keep
137/// thread-keyed types nameable.
138#[allow(dead_code)]
139#[derive(Eq, PartialEq, Clone, Copy, Hash, Debug)]
140pub struct ThreadId(core::num::NonZeroU64);
141
142/// A cell that provides lazy one-time initialization that implements [Sync] and [Send].
143///
144/// This module is a stub when no std is available to swap with [`std::sync::OnceLock`].
145pub struct SyncOnceCell<T>(OnceImported<T>);
146
147impl<T> Default for SyncOnceCell<T> {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl<T> SyncOnceCell<T> {
154 /// Create a new once.
155 #[inline(always)]
156 pub fn new() -> Self {
157 Self(OnceImported::new())
158 }
159
160 /// Initialize the cell with a value.
161 #[inline(always)]
162 pub fn initialized(value: T) -> Self {
163 #[cfg(not(feature = "std"))]
164 {
165 let cell = OnceImported::initialized(value);
166 Self(cell)
167 }
168
169 #[cfg(feature = "std")]
170 {
171 let cell = OnceImported::new();
172 // Infallible: the cell was just created, so it is empty. Ignoring
173 // the `Err` is what keeps `T: Debug` off this whole impl.
174 let _ = cell.set(value);
175
176 Self(cell)
177 }
178 }
179
180 /// Gets the contents of the cell, initializing it with `f` if the cell
181 /// was empty.
182 #[inline(always)]
183 pub fn get_or_init<F>(&self, f: F) -> &T
184 where
185 F: FnOnce() -> T,
186 {
187 #[cfg(not(feature = "std"))]
188 {
189 self.0.call_once(f)
190 }
191
192 #[cfg(feature = "std")]
193 {
194 self.0.get_or_init(f)
195 }
196 }
197}
198
199#[cfg(all(test, feature = "std"))]
200mod tests {
201 use super::*;
202
203 /// Regression: a panic under a guard must not poison the lock — one
204 /// failed autotune must not take down every later kernel launch.
205 #[test]
206 fn poisoned_mutex_recovers() {
207 let mutex = Mutex::new(0u32);
208
209 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
210 let _guard = mutex.lock();
211 panic!("poison the lock");
212 }))
213 .unwrap_err();
214
215 *mutex.lock() += 1;
216 assert_eq!(*mutex.lock(), 1);
217 }
218
219 #[test]
220 fn poisoned_rwlock_recovers() {
221 let lock = RwLock::new(0u32);
222
223 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
224 let _guard = lock.write();
225 panic!("poison the lock");
226 }))
227 .unwrap_err();
228
229 assert_eq!(*lock.read(), 0);
230 }
231}