atomic_interval/
lib.rs

1//! A thread-safe tiny implementation of a *lock-free* interval/timer structure.
2//!
3//! ## Example
4//! ```
5//! use atomic_interval::AtomicIntervalLight;
6//! use std::time::Duration;
7//! use std::time::Instant;
8//!
9//! let period = Duration::from_secs(1);
10//! let atomic_interval = AtomicIntervalLight::new(period);
11//!
12//! let time_start = Instant::now();
13//! let elapsed = loop {
14//!     if atomic_interval.is_ticked() {
15//!         break time_start.elapsed();
16//!     }
17//! };
18//!
19//! println!("Elapsed: {:?}", elapsed);
20//!
21//! ```
22//!
23//! ## Memory Ordering
24//! Like other standard atomic types, [`AtomicInterval`] requires specifying how the memory
25//! accesses have to be synchronized.
26//!
27//! For more information see the [nomicon](https://doc.rust-lang.org/nomicon/atomics.html).
28//!
29//! ## AtomicIntervalLight
30//! [`AtomicIntervalLight`] is an [`AtomicInterval`]'s variant that does not guarantee
31//! any memory synchronization.
32#![warn(missing_docs)]
33
34use quanta::Clock;
35use std::sync::atomic::AtomicU64;
36use std::sync::atomic::Ordering;
37use std::time::Duration;
38
39/// It implements a timer. It allows checking a periodic interval.
40///
41/// This structure is meant to be shared across multiple threads and does not
42/// require additional sync wrappers. Generally, it can be used with
43/// [`Arc<AtomicInterval>`](https://doc.rust-lang.org/std/sync/struct.Arc.html).
44///
45/// If you want performance maximization (when you do *not* need memory ordering
46/// synchronization), [`AtomicIntervalLight`] is a relaxed variant of this class.
47pub struct AtomicInterval {
48    inner: AtomicIntervalImpl,
49}
50
51impl AtomicInterval {
52    /// Creates a new [`AtomicInterval`] with a fixed period interval.
53    ///
54    /// The first tick is not instantaneous at the creation of the interval. It means `period`
55    /// amount of time has to elapsed for the first tick.
56    pub fn new(period: Duration) -> Self {
57        Self {
58            inner: AtomicIntervalImpl::new(period),
59        }
60    }
61
62    /// The period set for this interval.
63    pub fn period(&self) -> Duration {
64        self.inner.period()
65    }
66
67    /// Changes the period of this interval.
68    pub fn set_period(&mut self, period: Duration) {
69        self.inner.set_period(period)
70    }
71
72    /// Checks whether the interval's tick expired.
73    ///
74    /// When it returns `true` then *at least* `period` amount of time has passed
75    /// since the last tick.
76    ///
77    /// When a period is passed (i.e., this function return `true`) the internal timer
78    /// is automatically reset for the next tick.
79    ///
80    /// It takes two Ordering arguments to describe the memory ordering.
81    /// `success` describes the required ordering when the period elapsed and the timer
82    /// has to be reset (*read-modify-write* operation).
83    /// `failures` describes the required ordering when the period is not passed yet.
84    ///
85    /// Using [`Ordering::Acquire`] as success ordering makes the store part of this operation
86    /// [`Ordering::Relaxed`], and using [`Ordering::Release`] makes the successful load
87    /// [`Ordering::Relaxed`].
88    /// The failure ordering can only be [`Ordering::SeqCst`], [`Ordering::Acquire`] or
89    /// [`Ordering::Relaxed`] and must be equivalent to or weaker than the success ordering.
90    ///
91    /// It can be used in a concurrency context: only one thread can tick the timer per period.
92    ///
93    /// # Example
94    /// ```
95    /// use atomic_interval::AtomicInterval;
96    /// use std::sync::atomic::Ordering;
97    /// use std::time::Duration;
98    /// use std::time::Instant;
99    ///
100    /// let atomic_interval = AtomicInterval::new(Duration::from_secs(1));
101    /// let time_start = Instant::now();
102    ///
103    /// let elapsed = loop {
104    ///    if atomic_interval.is_ticked(Ordering::Relaxed, Ordering::Relaxed) {
105    ///        break time_start.elapsed();
106    ///    }
107    /// };
108    ///
109    /// println!("Elapsed: {:?}", elapsed);
110    /// // Elapsed: 999.842446ms
111    /// ```
112    pub fn is_ticked(&self, success: Ordering, failure: Ordering) -> bool {
113        self.inner.is_ticked::<false>(success, failure).0
114    }
115}
116
117/// A relaxed version of [`AtomicInterval`]: for more information check that.
118///
119/// All [`Ordering`] are implicit: [`Ordering::Relaxed`].
120///
121/// On some architecture this version is allowed to spuriously fail.
122/// It means [`AtomicIntervalLight::is_ticked`] might return `false` even if
123/// the `period` amount of time has passed.
124/// It can result in more efficient code on some platforms.
125pub struct AtomicIntervalLight {
126    inner: AtomicIntervalImpl,
127}
128
129impl AtomicIntervalLight {
130    /// Creates a new [`AtomicIntervalLight`] with a fixed period interval.
131    pub fn new(period: Duration) -> Self {
132        Self {
133            inner: AtomicIntervalImpl::new(period),
134        }
135    }
136
137    /// The period set for this interval.
138    pub fn period(&self) -> Duration {
139        self.inner.period()
140    }
141
142    /// Changes the period of this interval.
143    pub fn set_period(&mut self, period: Duration) {
144        self.inner.set_period(period)
145    }
146
147    /// See [`AtomicInterval::is_ticked`].
148    pub fn is_ticked(&self) -> bool {
149        self.inner
150            .is_ticked::<true>(Ordering::Relaxed, Ordering::Relaxed)
151            .0
152    }
153}
154
155struct AtomicIntervalImpl {
156    period: Duration,
157    clock: Clock,
158    last_tick: AtomicU64,
159}
160
161impl AtomicIntervalImpl {
162    fn new(period: Duration) -> Self {
163        let clock = Clock::new();
164        let last_tick = AtomicU64::new(clock.raw());
165
166        Self {
167            period,
168            clock,
169            last_tick,
170        }
171    }
172
173    #[inline(always)]
174    fn set_period(&mut self, period: Duration) {
175        self.period = period
176    }
177
178    #[inline(always)]
179    fn period(&self) -> Duration {
180        self.period
181    }
182
183    #[inline(always)]
184    fn is_ticked<const WEAK_CMP: bool>(
185        &self,
186        success: Ordering,
187        failure: Ordering,
188    ) -> (bool, Duration) {
189        let current = self.last_tick.load(failure);
190        let elapsed = self.clock.delta(current, self.clock.raw());
191
192        if self.period <= elapsed
193            && ((!WEAK_CMP
194                && self
195                    .last_tick
196                    .compare_exchange(current, self.clock.raw(), success, failure)
197                    .is_ok())
198                || (WEAK_CMP
199                    && self
200                        .last_tick
201                        .compare_exchange_weak(current, self.clock.raw(), success, failure)
202                        .is_ok()))
203        {
204            (true, elapsed)
205        } else {
206            (false, elapsed)
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_ticks() {
217        utilities::test_ticks_impl::<false>();
218        utilities::test_ticks_impl::<true>();
219    }
220
221    mod utilities {
222        use super::*;
223
224        pub(super) fn test_ticks_impl<const WEAK_CMP: bool>() {
225            const NUM_TICKS: usize = 10;
226            const ERROR_TOLERANCE: f64 = 0.03; // 3%
227
228            let period = Duration::from_millis(10);
229            let atomic_interval = AtomicIntervalImpl::new(period);
230
231            for _ in 0..NUM_TICKS {
232                let elapsed = wait_for_atomic_interval::<WEAK_CMP>(&atomic_interval);
233                assert!(period <= elapsed);
234
235                let error = elapsed.as_secs_f64() / period.as_secs_f64() - 1_f64;
236                assert!(
237                    error <= ERROR_TOLERANCE,
238                    "Delay error {:.1}% (max: {:.1}%)",
239                    error * 100_f64,
240                    ERROR_TOLERANCE * 100_f64
241                );
242            }
243        }
244
245        fn wait_for_atomic_interval<const WEAK_CMP: bool>(
246            atomic_interval: &AtomicIntervalImpl,
247        ) -> Duration {
248            loop {
249                let (ticked, elapsed) =
250                    atomic_interval.is_ticked::<WEAK_CMP>(Ordering::Relaxed, Ordering::Relaxed);
251                if ticked {
252                    break elapsed;
253                }
254            }
255        }
256    }
257}