Skip to main content

atomic_backoff/
lib.rs

1//! # atomic-backoff
2//!
3//! Customizable backoff strategies for compare-and-swap loops and spin loops.
4//!
5//! Compare-and-swap (CAS) loops and spin loops can often be optimized by adding backoff at each
6//! iteration, i.e. waiting a bit before the next iteration, in order to reduce the contention on
7//! the CPU's cache lines.
8//!
9//! As the optimal backoff strategy depends on multiple factors, especially the expected
10//! contention, this crate provides a generic [`BackoffStrategy`] to help customize algorithms
11//! using CAS/spin loops. Typical backoff strategies like [`ExponentialBackoff`] are also provided.
12//!
13//! Atomic types are extended with [`try_update_with_backoff`]/[`update_with_backoff`] methods,
14//! mirroring their std `try_update`/`update` counterparts.
15//!
16//! For handwritten CAS loops, see [`BackoffStrategy::backoff_reload`] and [`BackoffState`];
17//! for spin loops, see [`BackoffStrategy::backoff_until`].
18//!
19//! # Examples
20//!
21//! ```rust
22//! use std::{
23//!     sync::atomic::{AtomicUsize, Ordering::Relaxed},
24//!     thread,
25//!     time::{Duration, Instant},
26//! };
27//!
28//! use atomic_backoff::{AtomicWithBackoffExt, BackoffStrategy, ExponentialBackoff, NoBackoff};
29//!
30//! fn parallel_increment<S: BackoffStrategy>(threads: usize, iterations: usize) -> Duration {
31//!     let counter = AtomicUsize::new(0);
32//!     let start = Instant::now();
33//!     thread::scope(|s| {
34//!         for _ in 0..threads {
35//!             s.spawn(|| {
36//!                 for _ in 0..iterations {
37//!                     counter.update_with_backoff(Relaxed, Relaxed, |x| x + 1, S::default());
38//!                 }
39//!             });
40//!         }
41//!     });
42//!     assert_eq!(counter.load(Relaxed), threads * iterations);
43//!     start.elapsed()
44//! }
45//!
46//! let no_backoff = parallel_increment::<NoBackoff>(4, 10_000);
47//! let exponential = parallel_increment::<ExponentialBackoff<6, 4>>(4, 10_000);
48//! println!("no backoff: {no_backoff:?}, exponential backoff: {exponential:?}");
49//! // no backoff: 2.08ms, exponential backoff: 646µs
50//! ```
51//!
52//! [`try_update_with_backoff`]: AtomicWithBackoffExt::try_update_with_backoff
53//! [`update_with_backoff`]: AtomicWithBackoffExt::update_with_backoff
54#![no_std]
55
56#[cfg(feature = "std")]
57extern crate std;
58
59use core::{hint::spin_loop, sync::atomic::Ordering};
60
61/// Backoff strategy to be used after an atomic compare-and-swap (CAS) failure and in spin loops.
62///
63/// Waiting before retrying a failed CAS can greatly reduce the contention on the atomic's cache
64/// line, and improve the performance of CAS loops.
65///
66/// Spin loops also benefit from backoff as it avoids keeping the CPU 100% busy while waiting,
67/// at little latency cost.
68pub trait BackoffStrategy: Default + Send + Sync + 'static {
69    /// Whether the strategy does backoff or not.
70    ///
71    /// Some algorithms may have a different behavior depending on whether backoff is used; for
72    /// example switching between an unbounded spin loop or a thread parking algorithm. This
73    /// constant can be used for this purpose.
74    ///
75    /// It should be set to `false` only for [`NoBackoff`].
76    const BACKOFF: bool = true;
77
78    /// Performs backoff and returns how the CAS should be retried.
79    ///
80    /// [`will_reload`](Self::will_reload) should also be implemented accordingly.
81    ///
82    /// In spin loops, the returned value can simply be ignored.
83    fn backoff(&mut self) -> RetryStrategy;
84
85    /// Returns `true` if the next call to [`backoff`](Self::backoff) will not return
86    /// [`RetryStrategy::NoReload`].
87    ///
88    /// This hint can be used to downgrade the failure ordering of the CAS to `Relaxed` when the
89    /// returned value is overwritten anyway.
90    #[inline]
91    fn will_reload(&self) -> bool {
92        false
93    }
94
95    /// Performs backoff after a failed CAS and reloads the atomic value according to the returned
96    /// [`RetryStrategy`].
97    ///
98    /// [`ReloadUntilUnchanged`] causes this function to loop until the value stops changing. If the
99    /// value returned by the failed CAS must be tested between reloads, use [`BackoffState`]
100    /// instead.
101    ///
102    /// # Example
103    ///
104    /// ```rust
105    /// # use core::sync::atomic::{AtomicUsize, Ordering};
106    /// # use atomic_backoff::BackoffStrategy;
107    /// #
108    /// fn update_with_backoff(
109    ///     atomic: &AtomicUsize,
110    ///     set_order: Ordering,
111    ///     fetch_order: Ordering,
112    ///     mut f: impl FnMut(usize) -> usize,
113    ///     mut strategy: impl BackoffStrategy,
114    /// ) -> usize {
115    ///     let mut current = atomic.load(fetch_order);
116    ///     loop {
117    ///         let failure_order = if strategy.will_reload() {
118    ///             Ordering::Relaxed
119    ///         } else {
120    ///             fetch_order
121    ///         };
122    ///         match atomic.compare_exchange_weak(current, f(current), set_order, failure_order) {
123    ///             Ok(x) => return x,
124    ///             Err(cur) => current = strategy.backoff_reload(cur, || atomic.load(fetch_order)),
125    ///         }
126    ///     }
127    /// }
128    /// ```
129    ///
130    /// [`ReloadUntilUnchanged`]: RetryStrategy::ReloadUntilUnchanged
131    #[inline]
132    fn backoff_reload<T: PartialEq, F: FnMut() -> T>(
133        &mut self,
134        mut current: T,
135        mut reload: F,
136    ) -> T {
137        loop {
138            match self.backoff() {
139                RetryStrategy::NoReload => return current,
140                RetryStrategy::Reload => return reload(),
141                RetryStrategy::ReloadUntilUnchanged => {
142                    let reloaded = reload();
143                    if reloaded == current {
144                        return current;
145                    }
146                    current = reloaded;
147                }
148            }
149        }
150    }
151
152    /// Loops until a condition is fulfilled, performing backoff at each iteration.
153    #[inline]
154    fn backoff_until<C: BackoffUntilCondition, F: FnMut() -> C>(&mut self, mut f: F) -> C::Result {
155        loop {
156            if let Some(res) = f().into_result() {
157                return res;
158            }
159            self.backoff();
160        }
161    }
162}
163
164/// Retry strategy of a failed atomic compare-and-swap (CAS).
165///
166/// It is returned by [`BackoffStrategy::backoff`] and tells what to do with the value returned
167/// by the failed CAS before retrying.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum RetryStrategy {
170    /// Retry with the value returned by the failed CAS.
171    NoReload,
172    /// Reload the atomic and retry the CAS with the up-to-date value.
173    Reload,
174    /// Reload the atomic and keep backing off while the value changes between reloads, then retry
175    /// with the up-to-date value.
176    ReloadUntilUnchanged,
177}
178
179/// A condition checked in [`BackoffStrategy::backoff_until`].
180///
181/// It should typically be a `bool` or an `Option<T>`.
182pub trait BackoffUntilCondition {
183    /// The result to return when the condition is satisfied.
184    type Result;
185    /// Converts the condition into a result to be returned.
186    fn into_result(self) -> Option<Self::Result>;
187}
188
189impl BackoffUntilCondition for bool {
190    type Result = ();
191    #[inline]
192    fn into_result(self) -> Option<Self::Result> {
193        if self { Some(()) } else { None }
194    }
195}
196
197impl<T> BackoffUntilCondition for Option<T> {
198    type Result = T;
199    #[inline]
200    fn into_result(self) -> Option<Self::Result> {
201        self
202    }
203}
204
205/// No backoff.
206///
207/// Retries immediately with the value returned by the failed CAS.
208#[derive(Debug, Default)]
209pub struct NoBackoff;
210
211impl BackoffStrategy for NoBackoff {
212    const BACKOFF: bool = false;
213    #[inline]
214    fn backoff(&mut self) -> RetryStrategy {
215        RetryStrategy::NoReload
216    }
217}
218
219/// Emits a [`spin_loop`] and reloads the atomic value before retrying the CAS.
220#[derive(Debug, Default)]
221pub struct SpinBackoff;
222
223impl BackoffStrategy for SpinBackoff {
224    #[inline]
225    fn backoff(&mut self) -> RetryStrategy {
226        spin_loop();
227        RetryStrategy::Reload
228    }
229
230    #[inline]
231    fn will_reload(&self) -> bool {
232        true
233    }
234}
235
236/// Performs exponential backoff.
237///
238/// Each backoff iteration `iter` (starting from 0) calls [`spin_loop`] `1 << iter.min(SPIN_LIMIT)`
239/// times.
240///
241/// During the first `UNTIL_UNCHANGED_LIMIT` backoff iterations, backoff continues until the
242/// atomic's value stops changing between reloads; after that, the CAS is retried after a single
243/// reload to avoid starvation.
244///
245/// After `YIELD_AFTER` backoff iterations (and if the `std` feature is enabled), [`yield_now`] is
246/// called instead of spinning.
247///
248/// For reference, [`crossbeam::utils::Backoff`] is equivalent to `ExponentialBackoff<6>` with
249/// `Backoff::spin`, and `ExponentialBackoff<10, 0, 7>` with `Backoff::snooze`. However,
250/// `UNTIL_UNCHANGED_LIMIT` should also be used in contended CAS loop to further reduce contention.
251///
252/// [`yield_now`]: https://doc.rust-lang.org/std/thread/fn.yield_now.html
253/// [`crossbeam::utils::Backoff`]: https://docs.rs/crossbeam/latest/crossbeam/utils/struct.Backoff.html
254#[derive(Debug, Default)]
255pub struct ExponentialBackoff<
256    const SPIN_LIMIT: usize,
257    const UNTIL_UNCHANGED_LIMIT: usize = 0,
258    const YIELD_AFTER: usize = { usize::MAX },
259> {
260    iter: usize,
261}
262
263impl<const SPIN_LIMIT: usize, const UNTIL_UNCHANGED_LIMIT: usize, const YIELD_AFTER: usize>
264    ExponentialBackoff<SPIN_LIMIT, UNTIL_UNCHANGED_LIMIT, YIELD_AFTER>
265{
266    const ASSERT_SPIN_LIMIT: () = assert!(
267        SPIN_LIMIT < usize::BITS as usize,
268        "SPIN_LIMIT must be lower than usize::BITS"
269    );
270
271    /// Starts an exponential backoff at the given iteration (starting from 0).
272    ///
273    /// This constructor can be used to skip the first smaller iterations.
274    pub fn starts_at(iter: usize) -> Self {
275        ExponentialBackoff { iter }
276    }
277
278    /// Returns the current count of backoff iterations performed.
279    ///
280    /// It can be used for example to switch to another algorithm, like thread parking, after a
281    /// given iteration count.
282    pub fn iter_count(&self) -> usize {
283        self.iter
284    }
285}
286
287impl<const SPIN_LIMIT: usize, const UNTIL_UNCHANGED_LIMIT: usize, const YIELD_AFTER: usize>
288    BackoffStrategy for ExponentialBackoff<SPIN_LIMIT, UNTIL_UNCHANGED_LIMIT, YIELD_AFTER>
289{
290    #[inline]
291    fn backoff(&mut self) -> RetryStrategy {
292        let () = Self::ASSERT_SPIN_LIMIT;
293        if cfg!(feature = "std") && self.iter >= YIELD_AFTER {
294            #[cfg(feature = "std")]
295            std::thread::yield_now();
296        } else {
297            for _ in 0..1usize << self.iter.min(SPIN_LIMIT) {
298                spin_loop();
299            }
300        }
301        self.iter = self.iter.saturating_add(1);
302        if self.iter <= UNTIL_UNCHANGED_LIMIT {
303            RetryStrategy::ReloadUntilUnchanged
304        } else {
305            RetryStrategy::Reload
306        }
307    }
308
309    #[inline]
310    fn will_reload(&self) -> bool {
311        true
312    }
313}
314
315/// A wrapper around a [`BackoffStrategy`] to be used in CAS loops when the atomic value must be
316/// checked after each reload.
317///
318/// Contrary to [`BackoffStrategy::backoff_reload`], it allows checking for a termination condition
319/// and early exiting the loop before performing the backoff.
320///
321/// In order to avoid code duplication with the checks after the reloads,
322/// [`BackoffState::backoff_reload`] should be called at every iteration of the CAS loop before the
323/// CAS. However, to avoid performing a backoff before any CAS failure, `BackoffState` is
324/// initialized as disabled, and enabled after the first `backoff_reload` call.
325///
326/// # Examples
327///
328/// ```rust
329/// # use core::sync::atomic::{AtomicUsize, Ordering};
330/// # use atomic_backoff::{BackoffState, BackoffStrategy};
331/// #
332/// fn try_update_with_backoff(
333///     atomic: &AtomicUsize,
334///     set_order: Ordering,
335///     fetch_order: Ordering,
336///     mut f: impl FnMut(usize) -> Option<usize>,
337///     strategy: impl BackoffStrategy,
338/// ) -> Result<usize, usize> {
339///     let mut backoff = BackoffState::new(strategy);
340///     let mut current = atomic.load(fetch_order);
341///     loop {
342///         // Check the termination condition before backing off.
343///         let new = f(current).ok_or(current)?;
344///         // If the value has been reloaded, `new` must be recomputed.
345///         if backoff.backoff_reload(&mut current, || atomic.load(fetch_order)) {
346///             continue;
347///         }
348///         match atomic.compare_exchange_weak(current, new, set_order, fetch_order) {
349///             Ok(x) => return Ok(x),
350///             Err(cur) => current = cur,
351///         }
352///     }
353/// }
354/// ```
355#[derive(Debug, Default)]
356pub struct BackoffState<S> {
357    strategy: S,
358    enabled: bool,
359}
360
361impl<S: BackoffStrategy> BackoffState<S> {
362    /// Creates a new `BackoffState` with the given backoff strategy.
363    ///
364    /// The backoff starts as disabled so the first iteration before any CAS failure doesn't wait.
365    pub fn new(strategy: S) -> Self {
366        Self {
367            strategy,
368            enabled: false,
369        }
370    }
371
372    /// Starts the backoff in enabled mode.
373    ///
374    /// It is useful when the first CAS iteration is inlined in a hot function, and the complete CAS
375    /// loop with the backoff is outlined in a cold function, so the backoff must start enabled
376    /// after a CAS failure.
377    pub fn enable(mut self) -> Self {
378        self.enabled = true;
379        self
380    }
381
382    /// Enables the backoff for the next iteration or perform a backoff if already enabled.
383    ///
384    /// Returns `true` if the current atomic value has been updated after a reload, in which case
385    /// the new atomic value should be recomputed before retrying the CAS.
386    ///
387    /// The backoff can be temporarily disabled after a reload triggered by
388    /// [`RetryStrategy::Reload`] in order to execute the CAS with the reloaded value at the next
389    /// iteration.
390    #[inline]
391    pub fn backoff_reload<T: PartialEq, F: FnOnce() -> T>(
392        &mut self,
393        current: &mut T,
394        reload: F,
395    ) -> bool {
396        if !self.enabled {
397            self.enabled = true;
398            return false;
399        }
400        let retry = self.strategy.backoff();
401        if retry == RetryStrategy::NoReload {
402            return false;
403        }
404        let reloaded = reload();
405        if reloaded == *current {
406            return false;
407        }
408        *current = reloaded;
409        self.enabled = retry == RetryStrategy::ReloadUntilUnchanged;
410        true
411    }
412}
413
414impl<S: BackoffStrategy> From<S> for BackoffState<S> {
415    fn from(strategy: S) -> Self {
416        Self::new(strategy)
417    }
418}
419
420/// An atomic type.
421pub trait Atomic {
422    /// The value of the atomic.
423    type Value: Copy + PartialEq;
424    /// Load the value of the atomic.
425    fn load(&self, ordering: Ordering) -> Self::Value;
426    /// Stores a value into the atomic if the current value is the same as the `current` value.
427    fn compare_exchange_weak(
428        &self,
429        current: Self::Value,
430        new: Self::Value,
431        success: Ordering,
432        failure: Ordering,
433    ) -> Result<Self::Value, Self::Value>;
434}
435
436/// Extension trait providing CAS loop methods using a given [`BackoffStrategy`].
437pub trait AtomicWithBackoffExt: Atomic {
438    /// Fetches the value, and applies a function to it that returns an optional new value.
439    fn try_update_with_backoff<S, F>(
440        &self,
441        set_order: Ordering,
442        fetch_order: Ordering,
443        mut f: F,
444        strategy: S,
445    ) -> Result<Self::Value, Self::Value>
446    where
447        S: BackoffStrategy,
448        F: FnMut(Self::Value) -> Option<Self::Value>,
449    {
450        let mut backoff = BackoffState::new(strategy);
451        let mut current = self.load(fetch_order);
452        loop {
453            let new = f(current).ok_or(current)?;
454            if backoff.backoff_reload(&mut current, || self.load(fetch_order)) {
455                continue;
456            }
457            match self.compare_exchange_weak(current, new, set_order, fetch_order) {
458                Ok(x) => return Ok(x),
459                Err(cur) => current = cur,
460            }
461        }
462    }
463
464    /// Fetches the value, applies a function to it that returns a new value.
465    fn update_with_backoff<S, F>(
466        &self,
467        set_order: Ordering,
468        fetch_order: Ordering,
469        mut f: F,
470        mut strategy: S,
471    ) -> Self::Value
472    where
473        S: BackoffStrategy,
474        F: FnMut(Self::Value) -> Self::Value,
475    {
476        let mut current = self.load(fetch_order);
477        loop {
478            let failure_order = if strategy.will_reload() {
479                Ordering::Relaxed
480            } else {
481                fetch_order
482            };
483            match self.compare_exchange_weak(current, f(current), set_order, failure_order) {
484                Ok(x) => return x,
485                Err(cur) => current = strategy.backoff_reload(cur, || self.load(fetch_order)),
486            }
487        }
488    }
489}
490
491impl<T: Atomic> AtomicWithBackoffExt for T {}
492
493macro_rules! impl_atomic {
494    ($($($atomic:ident)::+ $(<$t:ident>)? => $value:ty,)*) => {$(
495        impl$(<$t>)? Atomic for $($atomic)::+$(<$t>)? {
496            type Value = $value;
497
498            #[inline(always)]
499            fn load(&self, ordering: Ordering) -> Self::Value {
500                self.load(ordering)
501            }
502
503            #[inline(always)]
504            fn compare_exchange_weak(
505                &self,
506                current: Self::Value,
507                new: Self::Value,
508                success: Ordering,
509                failure: Ordering,
510            ) -> Result<Self::Value, Self::Value> {
511                self.compare_exchange_weak(current, new, success, failure)
512            }
513        }
514    )*};
515}
516
517macro_rules! impl_core_atomic {
518    ($($size:literal: $atomic:ident $(<$t:ident>)? => $value:ty,)*) => {$(
519        #[cfg(target_has_atomic = $size)]
520        impl_atomic!(core::sync::atomic::$atomic $(<$t>)? => $value,);
521    )*};
522}
523
524impl_core_atomic! {
525    "8": AtomicBool => bool,
526    "8": AtomicI8 => i8,
527    "8": AtomicU8 => u8,
528    "16": AtomicI16 => i16,
529    "16": AtomicU16 => u16,
530    "32": AtomicI32 => i32,
531    "32": AtomicU32 => u32,
532    "64": AtomicI64 => i64,
533    "64": AtomicU64 => u64,
534    "ptr": AtomicIsize => isize,
535    "ptr": AtomicUsize => usize,
536    "ptr": AtomicPtr<T> => *mut T,
537}
538
539#[cfg(feature = "portable-atomic")]
540macro_rules! impl_portable_atomic {
541    ($($cfg:ident: $atomic:ident $(<$t:ident>)? => $value:ty,)*) => {
542        portable_atomic::cfg_has_atomic_cas! {$(
543            portable_atomic::$cfg! {
544                impl_atomic!(portable_atomic::$atomic $(<$t>)? => $value,);
545            }
546        )*}
547    };
548}
549
550#[cfg(feature = "portable-atomic")]
551impl_portable_atomic! {
552    cfg_has_atomic_8: AtomicBool => bool,
553    cfg_has_atomic_8: AtomicI8 => i8,
554    cfg_has_atomic_8: AtomicU8 => u8,
555    cfg_has_atomic_16: AtomicI16 => i16,
556    cfg_has_atomic_16: AtomicU16 => u16,
557    cfg_has_atomic_32: AtomicI32 => i32,
558    cfg_has_atomic_32: AtomicU32 => u32,
559    cfg_has_atomic_64: AtomicI64 => i64,
560    cfg_has_atomic_64: AtomicU64 => u64,
561    cfg_has_atomic_128: AtomicI128 => i128,
562    cfg_has_atomic_128: AtomicU128 => u128,
563    cfg_has_atomic_ptr: AtomicIsize => isize,
564    cfg_has_atomic_ptr: AtomicUsize => usize,
565    cfg_has_atomic_ptr: AtomicPtr<T> => *mut T,
566}
567
568// loom only provides 64-bit atomics on 64-bit targets.
569#[cfg(loom)]
570macro_rules! impl_loom_atomic {
571    ($($($width:literal:)? $atomic:ident $(<$t:ident>)? => $value:ty,)*) => {$(
572        $(#[cfg(target_pointer_width = $width)])?
573        impl_atomic!(loom::sync::atomic::$atomic $(<$t>)? => $value,);
574    )*};
575}
576
577#[cfg(loom)]
578impl_loom_atomic! {
579    AtomicBool => bool,
580    AtomicI8 => i8,
581    AtomicU8 => u8,
582    AtomicI16 => i16,
583    AtomicU16 => u16,
584    AtomicI32 => i32,
585    AtomicU32 => u32,
586    "64": AtomicI64 => i64,
587    "64": AtomicU64 => u64,
588    AtomicIsize => isize,
589    AtomicUsize => usize,
590    AtomicPtr<T> => *mut T,
591}
592
593#[cfg(test)]
594mod tests {
595    extern crate std;
596
597    use core::sync::atomic::{AtomicUsize, Ordering::Relaxed};
598    use std::{sync::Arc, thread};
599
600    use crate::{AtomicWithBackoffExt, NoBackoff};
601
602    /// Spawns two threads incrementing the same atomic, initialized to 0,
603    /// and returns what each of them got.
604    fn increment_twice<R: Send + 'static>(
605        f: impl Fn(&AtomicUsize) -> R + Copy + Send + 'static,
606    ) -> [R; 2] {
607        let atomic = Arc::new(AtomicUsize::new(0));
608        let spawn = || {
609            let atomic = atomic.clone();
610            thread::spawn(move || f(&atomic))
611        };
612        let (t1, t2) = (spawn(), spawn());
613        [t1.join().unwrap(), t2.join().unwrap()]
614    }
615
616    #[test]
617    fn update() {
618        let results = increment_twice(|atomic| {
619            atomic.update_with_backoff(Relaxed, Relaxed, |x| x + 1, NoBackoff)
620        });
621        assert!(results == [0, 1] || results == [1, 0], "{:?}", results);
622    }
623
624    #[test]
625    fn try_update() {
626        let results = increment_twice(|atomic| {
627            let incr = |x| if x != 1 { Some(x + 1) } else { None };
628            atomic.try_update_with_backoff(Relaxed, Relaxed, incr, NoBackoff)
629        });
630        assert!(
631            results == [Ok(0), Err(1)] || results == [Err(1), Ok(0)],
632            "{:?}",
633            results
634        );
635    }
636}