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}