Initial commit
This commit is contained in:
commit
ea3b41eb1d
8 changed files with 429 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
|
@ -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"
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -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]
|
42
README.md
Normal file
42
README.md
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
```
|
28
examples/channel.rs
Normal file
28
examples/channel.rs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
93
src/lib.rs
Normal file
93
src/lib.rs
Normal file
|
@ -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<bool>,
|
||||
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)));
|
||||
}
|
||||
}
|
167
src/timed.rs
Normal file
167
src/timed.rs
Normal file
|
@ -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<Timer>,
|
||||
/// 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<TimedLockData>,
|
||||
}
|
||||
|
||||
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<Timer> {
|
||||
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());
|
||||
}
|
||||
}
|
82
src/timer.rs
Normal file
82
src/timer.rs
Normal file
|
@ -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<Duration> 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<Ordering> {
|
||||
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())
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue