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}