commit ea3b41eb1d1c99bbc7a34bf63a6750c309a9df5f Author: TudbuT Date: Sat Jul 13 23:58:49 2024 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e64a73f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "microlock" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..70fa8e1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "microlock" +description = "A crate for waiting: Small locks and other timing things!" +license = "MIT" +repository = "https://git.tudbut.de/tudbut/microlock" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b768f99 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# microlock + +Microlock is a smallish crate for waiting. It contains an UntimedLock suitable +for complex synchronization, and a TimedLock that can also function as such, +but additionally provides timer functionalities. + +## Use-case: + +The untimed lock can be used similarly to a channel, with added flexibility. +In this example, it simply replaces a channel, but more complex use-cases are +significantly less trivial with channels. + +```rs +use std::{ + collections::VecDeque, + sync::{Arc, Mutex}, + thread, +}; + +use microlock::{Lock, TimedLock, UntimedLock}; + +fn main() { + static LOCK: UntimedLock = UntimedLock::locked(); + let queue = Arc::new(Mutex::new(VecDeque::new())); + let queue2 = queue.clone(); + thread::spawn(move || loop { + LOCK.wait_here(); + println!("{}", queue2.lock().unwrap().pop_front().unwrap()); + LOCK.lock(); + }); + + let timer = TimedLock::unlocked(); + for i in 0..5 { + timer.lock_for_ms(100); + println!("Sending {i}..."); + queue.lock().unwrap().push_back(format!("Hello! {i}")); + LOCK.unlock(); + println!("Sent!"); + timer.wait_here(); + } +} +``` diff --git a/examples/channel.rs b/examples/channel.rs new file mode 100644 index 0000000..dfb7f21 --- /dev/null +++ b/examples/channel.rs @@ -0,0 +1,28 @@ +use std::{ + collections::VecDeque, + sync::{Arc, Mutex}, + thread, +}; + +use microlock::{Lock, TimedLock, UntimedLock}; + +fn main() { + static LOCK: UntimedLock = UntimedLock::locked(); + let queue = Arc::new(Mutex::new(VecDeque::new())); + let queue2 = queue.clone(); + thread::spawn(move || loop { + LOCK.wait_here(); + println!("{}", queue2.lock().unwrap().pop_front().unwrap()); + LOCK.lock(); + }); + + let timer = TimedLock::unlocked(); + for i in 0..5 { + timer.lock_for_ms(100); + println!("Sending {i}..."); + queue.lock().unwrap().push_back(format!("Hello! {i}")); + LOCK.unlock(); + println!("Sent!"); + timer.wait_here(); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..89eff8f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,93 @@ +mod timed; +pub mod timer; +pub use timed::*; +use timer::{Timed, Timer, TimerDuration}; + +use std::{ + sync::{Condvar, Mutex}, + time::Duration, +}; + +/// An untimed lock. This can be locked and unlocked, but it will never unlock +/// on its own (see [`TimedLock`] for an expiring lock). If a thread calls +/// wait_here on a locked lock, it will wait until the lock is unlocked by +/// another thread. +pub struct UntimedLock { + locked: Mutex, + condvar: Condvar, +} + +impl UntimedLock { + /// Creates a new untimed lock that is either unlocked or locked. + pub const fn new(locked: bool) -> Self { + Self { + locked: Mutex::new(locked), + condvar: Condvar::new(), + } + } + + /// Creates a new untimed lock that is unlocked. + pub const fn unlocked() -> Self { + Self::new(false) + } + + /// Creates a new untimed lock that is locked. + pub const fn locked() -> Self { + Self::new(true) + } +} + +pub trait Lock { + /// Returns if the lock is (still) locked. + fn is_locked(&self) -> bool; + /// Locks the lock indefinitely. In case of a timed one, the previous + /// target will be replaced without releasing the waiting threads. + fn lock(&self); + /// Unlocks the lock and releases all waiting threads. + fn unlock(&self); + /// Makes the current thread wait on this lock until it is + /// unlocked (or expires). + fn wait_here(&self); + /// Makes the current thread wait on this lock until it is unlocked (or + /// expires) or the timeout expires. + fn wait_here_for(&self, timeout: TimerDuration); + fn wait_here_for_ms(&self, timeout_ms: u64); +} + +impl Lock for UntimedLock { + fn is_locked(&self) -> bool { + *self.locked.lock().unwrap() + } + + fn lock(&self) { + *self.locked.lock().unwrap() = true; + } + + fn unlock(&self) { + *self.locked.lock().unwrap() = false; + self.condvar.notify_all(); + } + + fn wait_here(&self) { + let mut locked = self.locked.lock().unwrap(); + while *locked { + locked = self.condvar.wait(locked).unwrap(); + } + } + + fn wait_here_for(&self, timeout: TimerDuration) { + let mut locked = self.locked.lock().unwrap(); + let timer = Timer::new(timeout); + while *locked && !timer.has_elapsed() { + locked = self + .condvar + .wait_timeout(locked, timer.time_left().to_real()) + .unwrap() + .0; + } + } + + fn wait_here_for_ms(&self, timeout_ms: u64) { + self.wait_here_for(TimerDuration::Real(Duration::from_millis(timeout_ms))); + } +} diff --git a/src/timed.rs b/src/timed.rs new file mode 100644 index 0000000..af1bab8 --- /dev/null +++ b/src/timed.rs @@ -0,0 +1,167 @@ +use std::{sync::Mutex, time::Duration}; + +use crate::{ + timer::{Timed, Timer, TimerDuration}, + Lock, UntimedLock, +}; + +struct TimedLockData { + /// The timer governing when the lock should unlock. This is None if it is + /// in untimed mode or if it is not locked. + time: Option, + /// When calling lock(t) on an already locked lock, it will have to unlock + /// and relock the inner UntimedLock. This flag prevents exiting in such a + /// situation, so wait_here* is never exited prematurely. + remain_locked: bool, +} + +/// An untimed lock. This can be locked and unlocked, and it will unlock on +/// its own after a timeout, if specified (see [`UntimedLock`] for a +/// non-expiring lock). If a thread calls wait_here on a locked lock, it will +/// wait until the lock is unlocked by another thread, or the lock expires. +pub struct TimedLock { + /// The inner lock, which is used as the implementation for actual locking + /// calls and the `locked` state. + inner: UntimedLock, + /// The data specifying how the lock is currently meant to act. (Most + /// importantly, it contains the Timer) + data: Mutex, +} + +impl TimedLock { + /// Creates a new timed lock that is either unlocked or locked. + pub const fn new(locked: bool) -> Self { + Self { + inner: UntimedLock::new(locked), + data: Mutex::new(TimedLockData { + time: None, + remain_locked: false, + }), + } + } + + /// Creates a new timed lock that is unlocked. + pub const fn unlocked() -> Self { + Self::new(false) + } + + /// Creates a new timed lock that is locked. + pub const fn locked() -> Self { + Self::new(true) + } + + /// Returns the inner timer. This is None if the lock is in untimed mode + /// or if it is currently unlocked. The timer may already have elapsed + /// if the lock wasn't unlocked explicitly and nobody was waiting on it + /// to trigger an update. + pub fn get_timer(&self) -> Option { + self.data.lock().unwrap().time + } +} + +impl Timed for TimedLock { + fn has_elapsed(&self) -> bool { + !self.is_locked() + } + + /// Returns the current time left until unlocking based on the knowledge + /// the lock has. This may be incorrect if the lock is relocked later on. + fn time_left(&self) -> TimerDuration { + if !self.is_locked() { + return TimerDuration::Elapsed; + } + self.data + .lock() + .unwrap() + .time + .map_or(TimerDuration::Infinite, |x| x.time_left()) + } +} + +impl TimedLock { + /// Locks the lock for some duration. If the lock is already locked, the + /// old target will be replaced with the new one. This will not wake up + /// any waiting threads, even if relocking is necessary. + pub fn lock_for(&self, duration: TimerDuration) { + if self.is_locked() { + self.inner.unlock(); + } + let mut data = self.data.lock().unwrap(); + data.time = Some(Timer::new(duration)); + data.remain_locked = true; + self.inner.lock(); + drop(data); + } + + pub fn lock_for_ms(&self, duration_ms: u64) { + self.lock_for(TimerDuration::Real(Duration::from_millis(duration_ms))) + } +} + +impl Lock for TimedLock { + /// Checks if the lock should still be locked based on the time, then + /// returns that. This updating is NOT necessary to release waiting + /// threads. + fn is_locked(&self) -> bool { + let data = self.data.lock().unwrap(); + if !self.inner.is_locked() { + return false; + } + if let Some(time) = data.time { + if time.has_elapsed() { + drop(data); + self.unlock(); + return false; + } + } + true + } + + fn lock(&self) { + let mut data = self.data.lock().unwrap(); + data.time = None; + data.remain_locked = true; + self.inner.lock(); + drop(data); + } + + fn unlock(&self) { + let mut data = self.data.lock().unwrap(); + data.time = None; + data.remain_locked = false; + self.inner.unlock(); + drop(data); + } + + fn wait_here(&self) { + loop { + if let Some(time) = self.get_timer() { + self.inner.wait_here_for(time.time_left()); + } else { + self.inner.wait_here(); + } + if !self.is_locked() || !self.data.lock().unwrap().remain_locked { + break; + } + } + } + + fn wait_here_for(&self, timeout: TimerDuration) { + let timer = Timer::new(timeout); + loop { + if let Some(time) = self.get_timer() { + self.inner + .wait_here_for(time.time_left().min(timer.time_left())); + } else { + self.inner.wait_here_for(timer.time_left()); + } + if !self.is_locked() || !self.data.lock().unwrap().remain_locked { + break; + } + } + } + + fn wait_here_for_ms(&self, timeout_ms: u64) { + self.wait_here_for(Duration::from_millis(timeout_ms).into()); + } +} diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..a6ae3ff --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,82 @@ +use std::{ + cmp::Ordering, + time::{Duration, Instant}, +}; + +pub const INFINITE_DURATION: Duration = Duration::new(u64::MAX, 1_000_000_000 - 1); + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum TimerDuration { + Elapsed, + Real(Duration), + Infinite, +} +impl TimerDuration { + pub fn from_difference(a: TimerDuration, b: Duration) -> TimerDuration { + let a = a.to_real(); + if a < b { + TimerDuration::Elapsed + } else { + TimerDuration::Real(a - b) + } + } + + pub fn to_real(&self) -> Duration { + match *self { + Self::Real(d) if d > Duration::ZERO => d, + Self::Infinite => INFINITE_DURATION, + _ => Duration::new(0, 0), + } + } +} +impl From for TimerDuration { + fn from(value: Duration) -> Self { + if value == Duration::ZERO { + return Self::Elapsed; + } + Self::Real(value) + } +} +impl PartialOrd for TimerDuration { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for TimerDuration { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (TimerDuration::Elapsed, TimerDuration::Elapsed) => Ordering::Equal, + (TimerDuration::Elapsed, _) => Ordering::Less, + (TimerDuration::Real(_), TimerDuration::Elapsed) => Ordering::Greater, + (TimerDuration::Real(a), TimerDuration::Real(b)) => a.cmp(b), + (TimerDuration::Real(_), TimerDuration::Infinite) => Ordering::Less, + (TimerDuration::Infinite, TimerDuration::Infinite) => Ordering::Equal, + (TimerDuration::Infinite, _) => Ordering::Greater, + } + } +} + +#[derive(Clone, Copy)] +pub struct Timer(Instant, TimerDuration); +impl Timer { + pub fn new(d: TimerDuration) -> Self { + Self(Instant::now(), d) + } + + pub fn time_elapsed(&self) -> Duration { + self.0.elapsed() + } +} + +pub trait Timed { + fn has_elapsed(&self) -> bool; + fn time_left(&self) -> TimerDuration; +} +impl Timed for Timer { + fn has_elapsed(&self) -> bool { + self.0.elapsed() >= self.1.to_real() + } + fn time_left(&self) -> TimerDuration { + TimerDuration::from_difference(self.1, self.0.elapsed()) + } +}