├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── examples └── countdown.rs └── src ├── countdown.rs ├── lib.rs └── semaphore.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | README.pdf 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsevents-extra" 3 | description = "Synchronization objects built on top of rsevents. Semaphore, countdown event, and more." 4 | version = "0.2.2" 5 | edition = "2018" 6 | authors = ["Mahmoud Al-Qudsi ", 7 | "NeoSmart Technologies "] 8 | homepage = "https://github.com/neosmart/rsevents-extra" 9 | repository = "https://github.com/neosmart/rsevents-extra" 10 | readme = "README.md" 11 | keywords = ["synchronization", "events", "countdown"] 12 | categories = ["concurrency", "asynchronous"] 13 | license = "MIT" 14 | documentation = "https://docs.rs/rsevents-extra/latest/rsevents_extra/" 15 | 16 | [dependencies] 17 | rsevents = { version = "0.3.1" } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Developed and maintained by Mahmoud Al-Qudsi 4 | Copyright (c) 2018 NeoSmart Technologies 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # CargoMake by NeoSmart Technologies 2 | # Written and maintained by Mahmoud Al-Qudsi 3 | # Released under the MIT public license 4 | # Obtain updates from https://github.com/neosmart/CargoMake 5 | 6 | COLOR ?= always # Valid COLOR options: {always, auto, never} 7 | CARGO = cargo --color $(COLOR) 8 | 9 | .PHONY: all bench build check clean doc install publish run test update 10 | 11 | all: build 12 | 13 | bench: 14 | @$(CARGO) bench 15 | 16 | build: 17 | @$(CARGO) build 18 | 19 | check: build test 20 | 21 | clean: 22 | @$(CARGO) clean 23 | 24 | doc: 25 | @$(CARGO) doc 26 | 27 | install: build 28 | @$(CARGO) install 29 | 30 | publish: 31 | @$(CARGO) publish 32 | 33 | run: build 34 | @$(CARGO) run 35 | 36 | test: build 37 | @$(CARGO) test 38 | 39 | update: 40 | @$(CARGO) update 41 | 42 | miri: 43 | @env MIRIFLAGS=-Zmiri-disable-isolation\ -Zmiri-permissive-provenance\ \ 44 | $(CARGO) miri test -- --nocapture 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rsevents-extra 2 | 3 | [![crates.io](https://img.shields.io/crates/v/rsevents-extra.svg)](https://crates.io/crates/rsevents-extra) 4 | [![docs.rs](https://docs.rs/rsevents-extra/badge.svg)](https://docs.rs/rsevents-extra/latest/rsevents_extra) 5 | 6 | `rsevents-extra` is a utility crate with a number of useful synchronization "primitives" built on top of (and therefore, at a higher level than) [`rsevents`](https://github.com/neosmart/rsevents/). 7 | `rsevents-extra` is a community project, feel free to contribute additional synchronization objects to this crate! 8 | 9 | ## About `rsevents` 10 | 11 | Please refer to the `rsevents` [README](https://github.com/neosmart/rsevents/) and [documentation](https://docs.rs/rsevents/latest/rsevents/) to learn more about `rsevents`, the library that this crate is built on top of. 12 | At its core, `rsevents` is a low-level signalling and synchronization library, mimicking the behavior of the WIN32 auto- and manual-reset events, and can be useful to add lightweight and performant synchronization to programs where the needs do not strictly align with the concepts of mutexes or critical sections. 13 | 14 | This crate includes some additional synchronization types built on top of the core events in the `rsevents` library. 15 | 16 | ## Utility events included in this crate 17 | 18 | This crate contains implementations of the following events: 19 | 20 | * Countdown Event 21 | * Semaphore 22 | 23 | ### Countdown Event 24 | 25 | A countdown event is a useful synchronization tool for spawning tasks and checking on their completion status. 26 | A `CountdownEvent` object is instantiated with a count, and upon each call to `CountdownEvent::tick()`, the internal count is decremented. 27 | A waiter can call `CountdownEvent::wait()` (or any of the other wait routines exposed by the `Awaitable` trait) to block efficiently until the countdown reaches zero. 28 | Once the internal countdown reaches zero, the event becomes set and waiters are woken/notified and the event remains set until a call to `CountdownEvent::reset()` is made. 29 | 30 | ### Semaphore 31 | 32 | A semaphore is a synchronization primitive used to limit concurrency or concurrent access to a particular resource or region. 33 | A semaphore created with `Semaphore::new()` is assigned both a maximum concurrency and an initial concurrency (up to the maximum). 34 | Threads obtain a concurrency token by calling `Semaphore::wait()`, which reserves them a slot to access the concurrency-limited region until the concurrency token is dropped at the end of the scope. 35 | If more threads attempt to obtain access to a semaphore-protected region, their calls to `Semaphore::wait()` will block (while they efficiently sleep) until another thread drops its concurrency token or the semaphore's concurrency limit is increased. 36 | -------------------------------------------------------------------------------- /examples/countdown.rs: -------------------------------------------------------------------------------- 1 | // rsevents_extra re-exports rsevents::Awaitable 2 | use rsevents_extra::{Awaitable, CountdownEvent}; 3 | use std::{thread, time::Duration}; 4 | 5 | fn main() { 6 | let countdown = CountdownEvent::new(42); 7 | 8 | thread::scope(|scope| { 9 | // Start two worker threads to each do some of the work 10 | for i in 0..2 { 11 | // Shadow some variables to allow us to `move` into the closure 12 | let i = i; 13 | let countdown = &countdown; 14 | 15 | scope.spawn(move || { 16 | println!("Worker thread reporting for duty!"); 17 | 18 | // Worker thread will pretend to do some work 19 | for i in (500 * i)..(280 * (i + 1)) { 20 | if i % 7 == 3 { 21 | countdown.tick(); 22 | } 23 | 24 | thread::sleep(Duration::from_millis(18)); 25 | } 26 | }); 27 | } 28 | 29 | // The main thread will wait for 42 tasks to be completed before it does 30 | // its thing... whatever that is. 31 | while !countdown.wait_for(Duration::from_secs(1)) { 32 | // Report progress every 1 second until we've finished 33 | eprintln!("Work in progress. {} items remaining.", countdown.count()); 34 | } 35 | 36 | eprintln!("Work completed!"); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/countdown.rs: -------------------------------------------------------------------------------- 1 | use rsevents::{AutoResetEvent, Awaitable, EventState, ManualResetEvent, TimeoutError}; 2 | use std::convert::{Infallible, TryInto}; 3 | use std::sync::atomic::{AtomicIsize, Ordering}; 4 | use std::time::Duration; 5 | 6 | /// An `Awaitable` type that can be used to block until _n_ parallel tasks have completed. 7 | /// 8 | /// A countdown event is a special type of [`ManualResetEvent`] that makes it easy to wait for a 9 | /// given number of tasks to complete asynchronously, and then carry out some action. A countdown 10 | /// event is first initialized with a count equal to the number of outstanding tasks, and each time 11 | /// a task is completed, [`CountdownEvent::tick()`] is called. A call to [`CountdownEvent::wait()`] 12 | /// will block until all outstanding tasks have completed and the internal counter reaches 0. 13 | /// 14 | /// Countdown events are thread-safe and may be declared as static variables or wrapped in an 15 | /// [`Arc`](std::sync::Arc) to easily share across threads. 16 | /// 17 | /// ## Example: 18 | /// 19 | /// ```rust 20 | /// use rsevents_extra::{Awaitable, CountdownEvent}; 21 | /// 22 | /// // CountdownEvent::new() is const and can be used directly in a static 23 | /// // context without needing lazy_static or once_cell: 24 | /// static ALMOST_DONE: CountdownEvent = CountdownEvent::new(0); 25 | /// 26 | /// fn worker_thread() { 27 | /// for _ in 0..250 { 28 | /// // 29 | /// 30 | /// // Each time we've finished a task, report our progress against 31 | /// // the countdown event. 32 | /// // Note that it's OK to keep calling this after the countdown 33 | /// // event has already fired. 34 | /// ALMOST_DONE.tick(); 35 | /// } 36 | /// } 37 | /// 38 | /// fn main() { 39 | /// // Re-init the countdown event to fire after the first 750 tasks have 40 | /// // been completed. 41 | /// ALMOST_DONE.reset(750); 42 | /// 43 | /// // Start 4 threads to begin work in parallel 44 | /// std::thread::scope(|scope| { 45 | /// for _ in 0..4 { 46 | /// scope.spawn(worker_thread); 47 | /// } 48 | /// 49 | /// // Wait for the 750 tasks to be finished. This gives us more 50 | /// // freedom than blocking until all threads have exited (as they may 51 | /// // be long-lived and service many different tasks of different 52 | /// // types, each of which we could track separately.) 53 | /// ALMOST_DONE.wait(); 54 | /// 55 | /// eprintln!("Worker threads have almost finished! Hang tight!"); 56 | /// }); 57 | /// } 58 | /// ``` 59 | pub struct CountdownEvent { 60 | /// The internal count tracking the number of events left. While we could use an unsigned type 61 | /// and just wrap on under/overflow and that would be fine (since we only set the event in 62 | /// response to a `tick()` call and never reset it), it means calls to `CountdownEvent::count()` 63 | /// would report the overflow and we couldn't intercept it. 64 | count: AtomicIsize, 65 | /// The core synchronization event, waited on by calls to `wait()` but only accessed on the 66 | /// final call to `tick()`. 67 | event: ManualResetEvent, 68 | /// The event used to adjudicate disputes between calls to `reset()` or `increment()` coinciding 69 | /// with the final call to `tick()`. 70 | event2: AutoResetEvent, 71 | } 72 | 73 | impl CountdownEvent { 74 | /// Creates a new countdown event with the internal count initialized to `count`. If a count of 75 | /// zero is specified, the event is immediately set. 76 | /// 77 | /// This is a `const` function and can be used in a `static` context, (e.g. to declare a shared, 78 | /// static variable without using lazy_static or once_cell). 79 | pub const fn new(count: usize) -> Self { 80 | const MAX: usize = isize::MAX as usize; 81 | let count: isize = match count { 82 | 0..=MAX => count as isize, 83 | _ => panic!("count cannot exceeed isize::MAX"), 84 | }; 85 | 86 | Self { 87 | count: AtomicIsize::new(count), 88 | event: ManualResetEvent::new(if count == 0 { 89 | EventState::Set 90 | } else { 91 | EventState::Unset 92 | }), 93 | event2: AutoResetEvent::new(EventState::Set), 94 | } 95 | } 96 | 97 | /// Decrements the internal countdown. When the internal countdown reaches zero, the countdown 98 | /// event enters a [set](EventState::Set) state and any outstanding or future calls to 99 | /// [`CountdownEvent::wait()`] will be let through without blocking (until [the event is 100 | /// reset](CountdownEvent::reset()) [or incremented](Self::increment())). 101 | pub fn decrement(&self) { 102 | let prev = self.count.fetch_sub(1, Ordering::Relaxed); 103 | if prev == 1 { 104 | self.event2.wait(); 105 | if self.count.load(Ordering::Relaxed) == 0 { 106 | self.event.set(); 107 | } 108 | self.event2.set(); 109 | } 110 | #[cfg(debug_assertions)] 111 | if prev == 0 { 112 | panic!("tick() called more times than outstanding jobs!"); 113 | } 114 | } 115 | 116 | /// An alias for [`decrement()`](Self::decrement) for backwards compatibility purposes. 117 | #[inline(always)] 118 | pub fn tick(&self) { 119 | self.decrement() 120 | } 121 | 122 | /// Increment the internal count (e.g. to add a work item). 123 | /// 124 | /// This resets the event (makes it unavailable) if the previous count was zero. 125 | pub fn increment(&self) { 126 | let prev = self.count.fetch_add(1, Ordering::Relaxed); 127 | if prev == 0 { 128 | self.event2.wait(); 129 | if self.count.load(Ordering::Relaxed) == 0 { 130 | self.event.set(); 131 | } 132 | self.event2.set(); 133 | } 134 | } 135 | 136 | /// Resets a countdown event to the specified `count`. If a count of zero is specified, the 137 | /// countdown event is immediately set. 138 | pub fn reset(&self, count: usize) { 139 | let count: isize = match count.try_into() { 140 | Ok(count) => count, 141 | Err(_) => panic!("count cannot exceeed isize::MAX"), 142 | }; 143 | 144 | self.count.store(count, Ordering::Relaxed); 145 | if self.count.load(Ordering::Relaxed) == 0 { 146 | self.event2.wait(); 147 | if self.count.load(Ordering::Relaxed) == 0 { 148 | self.event.set(); 149 | } 150 | self.event2.set(); 151 | self.event.set(); 152 | } 153 | } 154 | 155 | /// Get the current internal countdown value. 156 | pub fn count(&self) -> usize { 157 | match self.count.load(Ordering::Relaxed) { 158 | count @ 0.. => count as usize, 159 | _ => 0, 160 | } 161 | } 162 | } 163 | 164 | impl Awaitable<'_> for CountdownEvent { 165 | type T = (); 166 | type Error = TimeoutError; 167 | 168 | /// Waits for the internal countdown of the [`CountdownEvent`] to reach zero. 169 | fn try_wait(&self) -> Result<(), Infallible> { 170 | self.event.try_wait() 171 | } 172 | 173 | /// Waits for the internal countdown of the [`CountdownEvent`] to reach zero or returns an error 174 | /// in case of a timeout. 175 | fn try_wait_for(&self, limit: Duration) -> Result<(), TimeoutError> { 176 | self.event.try_wait_for(limit) 177 | } 178 | 179 | /// An optimized (wait-free, lock-free) check to see if the `CountdownEvent` has reached zero or 180 | /// not. 181 | fn try_wait0(&self) -> Result<(), TimeoutError> { 182 | self.event.try_wait0() 183 | } 184 | } 185 | 186 | #[test] 187 | fn basic_countdown() { 188 | let countdown = CountdownEvent::new(1); 189 | assert_eq!(countdown.wait0(), false); 190 | countdown.tick(); 191 | assert_eq!(countdown.wait0(), true); 192 | } 193 | 194 | #[test] 195 | fn reset_countdown() { 196 | let countdown = CountdownEvent::new(1); 197 | assert_eq!(countdown.wait0(), false); 198 | countdown.tick(); 199 | assert_eq!(countdown.wait0(), true); 200 | countdown.reset(1); 201 | assert_eq!(countdown.wait0(), false); 202 | } 203 | 204 | #[test] 205 | fn start_at_zero() { 206 | let countdown = CountdownEvent::new(0); 207 | assert_eq!(countdown.wait0(), true); 208 | } 209 | 210 | #[test] 211 | fn threaded_countdown() { 212 | use std::thread; 213 | 214 | static COUNTDOWN: CountdownEvent = CountdownEvent::new(2); 215 | 216 | assert_eq!(COUNTDOWN.wait0(), false); 217 | 218 | let thread1 = thread::spawn(move || { 219 | assert_eq!(COUNTDOWN.wait0(), false); 220 | COUNTDOWN.tick(); 221 | }); 222 | 223 | let thread2 = thread::spawn(move || { 224 | assert_eq!(COUNTDOWN.wait0(), false); 225 | COUNTDOWN.tick(); 226 | }); 227 | 228 | COUNTDOWN.wait(); 229 | 230 | // To catch any panics 231 | thread1.join().unwrap(); 232 | thread2.join().unwrap(); 233 | } 234 | 235 | #[test] 236 | fn negative_countdown() { 237 | let countdown = CountdownEvent::new(1); 238 | assert_eq!(false, countdown.wait0()); 239 | countdown.tick(); 240 | assert_eq!(countdown.count(), 0); 241 | assert_eq!(true, countdown.wait0()); 242 | countdown.tick(); 243 | assert_eq!(countdown.count(), 0); 244 | assert_eq!(true, countdown.wait0()); 245 | } 246 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod countdown; 2 | mod semaphore; 3 | 4 | pub use self::countdown::CountdownEvent; 5 | pub use self::semaphore::{Semaphore, SemaphoreGuard}; 6 | 7 | /// The `rsevents` abstraction over all types that can be awaited, implemented by types in this 8 | /// crate. 9 | /// 10 | pub use rsevents::Awaitable; 11 | /// The default `rsevents` error for `Awaitable` implementations in this crate, indicating that 12 | /// unbounded calls to [`Awaitable::wait()`] cannot fail. 13 | /// 14 | pub use rsevents::TimeoutError; 15 | -------------------------------------------------------------------------------- /src/semaphore.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::bool_assert_comparison)] 2 | #![allow(clippy::absurd_extreme_comparisons)] 3 | 4 | use rsevents::{AutoResetEvent, Awaitable, EventState, TimeoutError}; 5 | use std::convert::Infallible; 6 | use std::fmt::Debug; 7 | use std::sync::atomic::{AtomicU16, Ordering}; 8 | use std::time::Duration; 9 | 10 | type Count = u16; 11 | type AtomicCount = AtomicU16; 12 | type ICount = i16; 13 | type INext = i32; 14 | 15 | /// A concurrency-limiting synchronization primitive, used to limit the number of threads 16 | /// performing a certain operation or accessing a particular resource at the same time. 17 | /// 18 | /// A `Semaphore` is created with a maximum concurrency count that can never be exceeded, and an 19 | /// initial concurrency count that determines the available concurrency at creation. Threads 20 | /// attempting to access a limited-concurrency resource or perform a concurrency-limited operation 21 | /// [wait on the `Semaphore`](Semaphore::wait()), an operation which either immediately grants 22 | /// access to the calling thread if the available concurrency has not been saturated or blocks, 23 | /// sleeping the thread until another thread completes its concurrency-limited operation or the 24 | /// available concurrency limit is further increased. 25 | /// 26 | /// While the available concurrency count may be modified (decremented to zero or incremented up to 27 | /// the maximum specified at the time of its instantiation), the maximum concurrency limit cannot be 28 | /// changed once the `Semaphore` has been created. 29 | /// 30 | /// ## Example: 31 | /// 32 | /// ```no_run 33 | /// use rsevents_extra::{Semaphore}; 34 | /// use std::sync::atomic::{AtomicU32, Ordering}; 35 | /// 36 | /// // Limit maximum number of simultaneous network requests to 4, but start 37 | /// // with only 1 simultaneous network request allowed. 38 | /// const MAX_REQUESTS: u16 = 4; 39 | /// const START_REQUESTS: u16 = 1; 40 | /// static HTTP_SEM: Semaphore = Semaphore::new(START_REQUESTS, MAX_REQUESTS); 41 | /// static TASKS_LEFT: AtomicU32 = AtomicU32::new(42); 42 | /// 43 | /// fn download_file(url: &str) -> Result, std::io::Error> { 44 | /// // Make sure we never exceed the maximum number of simultaneous 45 | /// // network connections allowed. 46 | /// let sem_guard = HTTP_SEM.wait(); 47 | /// 48 | /// // 49 | /// 50 | /// // When `sem_guard` is dropped at the end of the scope, we give up our 51 | /// // network access slot letting another thread through. 52 | /// return Ok(unimplemented!()); 53 | /// } 54 | /// 55 | /// fn get_file_from_cache(url: &str) -> Result, ()> { todo!() } 56 | /// 57 | /// fn do_work() -> Result<(), std::io::Error> { 58 | /// loop { 59 | /// let mut file_in_cache = false; 60 | /// // Do some stuff that takes time here... 61 | /// // ... 62 | /// let url = "https://some-url/some/path/"; 63 | /// let file = get_file_from_cache(url).or_else(|_| download_file(url))?; 64 | /// // Do something with the file... 65 | /// // ... 66 | /// TASKS_LEFT.fetch_sub(1, Ordering::Relaxed); 67 | /// } 68 | /// } 69 | /// 70 | /// fn main() { 71 | /// // Start a thread to read control messages from the user 72 | /// std::thread::spawn(|| { 73 | /// let mut network_limit = START_REQUESTS; 74 | /// loop { 75 | /// println!("Press f to go faster or s to go slower"); 76 | /// let mut input = String::new(); 77 | /// std::io::stdin().read_line(&mut input).unwrap(); 78 | /// match input.trim() { 79 | /// "f" if network_limit < MAX_REQUESTS => { 80 | /// HTTP_SEM.release(1); 81 | /// network_limit += 1; 82 | /// } 83 | /// "s" if network_limit > 0 => { 84 | /// HTTP_SEM.wait().forget(); 85 | /// network_limit -= 1; 86 | /// } 87 | /// _ => eprintln!("Invalid request!"), 88 | /// } 89 | /// } 90 | /// }); 91 | /// 92 | /// // Start 8 worker threads and wait for them to finish 93 | /// std::thread::scope(|scope| { 94 | /// for _ in 0..8 { 95 | /// scope.spawn(do_work); 96 | /// } 97 | /// }); 98 | /// } 99 | /// ``` 100 | pub struct Semaphore { 101 | /// The maximum available concurrency for this semaphore, set at the time of initialization and 102 | /// static thereafter. 103 | max: Count, 104 | /// The current available concurrency for this semaphore, `> 0 && <= max`. This is like 105 | /// `current` but it also includes "currently borrowed" semaphore instances. The only reason for 106 | /// this field to exist is so that a truly safe `Semaphore::try_release()` method can exist (one 107 | /// that can guarantee not only that the new `count` won't exceed `max`, but also that the 108 | /// release operation will never cause `count` to exceed `max` even after all borrowed semaphore 109 | /// slots are returned. 110 | current: AtomicCount, 111 | /// The currently available concurrency count, equal to `current` minus any borrowed/obtained 112 | /// semaphore slots. 113 | count: AtomicCount, 114 | /// The auto-reset event used to sleep awaiting threads until a zero concurrency count is 115 | /// incremented, waking only one awaiter at a time. 116 | event: AutoResetEvent, 117 | } 118 | 119 | enum Timeout { 120 | /// Return immediately, 121 | None, 122 | /// Wait indefinitely, 123 | Infinite, 124 | /// Wait for the duration to elapse 125 | Bounded(Duration), 126 | } 127 | 128 | impl Semaphore { 129 | /// Create a new [`Semaphore`] with a maximum available concurrency count of `max_count` 130 | /// and an initial available concurrency count of `initial_count`. 131 | pub const fn new(initial_count: Count, max_count: Count) -> Self { 132 | #[allow(unused_comparisons)] 133 | if max_count < 0 { 134 | panic!("Invalid max_count < 0"); 135 | } 136 | #[allow(unused_comparisons)] 137 | if initial_count < 0 { 138 | panic!("Invalid initial_count < 0"); 139 | } 140 | if initial_count > max_count { 141 | panic!("Invalid initial_count > max_count"); 142 | } 143 | 144 | Semaphore { 145 | max: max_count, 146 | current: AtomicCount::new(initial_count), 147 | count: AtomicCount::new(initial_count as Count), 148 | event: AutoResetEvent::new(EventState::Unset), 149 | } 150 | } 151 | 152 | fn try_wait(&self, timeout: Timeout) -> Result<(), TimeoutError> { 153 | let mut count = self.count.load(Ordering::Relaxed); 154 | 155 | loop { 156 | #[allow(unused_comparisons)] 157 | if count < 0 { 158 | debug_assert!(false, "Count cannot be less than zero!"); 159 | } 160 | debug_assert!(count <= self.max); 161 | 162 | count = if count == 0 { 163 | // eprintln!("Semaphore unavailable. Sleeping until the event is signalled."); 164 | match timeout { 165 | Timeout::None => return Err(TimeoutError), 166 | Timeout::Infinite => self.event.try_wait()?, 167 | Timeout::Bounded(timeout) => self.event.try_wait_for(timeout)?, 168 | } 169 | 170 | self.count.load(Ordering::Relaxed) 171 | } else { 172 | // We can't just fetch_sub(1) and check the result because we might underflow. 173 | match self.count.compare_exchange_weak( 174 | count, 175 | count - 1, 176 | Ordering::Acquire, 177 | Ordering::Relaxed, 178 | ) { 179 | Ok(_) => { 180 | // We obtained the semaphore. 181 | let new_count = count - 1; 182 | // eprintln!("Semaphore available. New count: {new_count}"); 183 | if new_count > 0 { 184 | self.event.set(); 185 | } 186 | break; 187 | } 188 | Err(count) => count, 189 | } 190 | } 191 | } 192 | 193 | #[allow(unused_comparisons)] 194 | if count < 0 { 195 | debug_assert!(false, "Count cannot be less than zero!"); 196 | } 197 | debug_assert!(count <= self.max); 198 | 199 | Ok(()) 200 | } 201 | 202 | /// Attempts to obtain access to the resource or code protected by the `Semaphore`, subject to 203 | /// the available concurrency count. Returns immediately if the `Semaphore`'s internal 204 | /// concurrency count is non-zero or blocks sleeping until the `Semaphore` becomes available 205 | /// (via another thread completing its access to the controlled-concurrency region or if the 206 | /// semaphore's concurrency limit is raised). 207 | /// 208 | /// A successful wait against the semaphore decrements its internal available concurrency 209 | /// count (possibly preventing other threads from obtaining the semaphore) until 210 | /// [`Semaphore::release()`] is called (which happens automatically when the `SemaphoreGuard` 211 | /// concurrency token is dropped). 212 | #[must_use = "The semaphore count is immediately re-incremented if the guard is dropped"] 213 | pub fn wait(&self) -> SemaphoreGuard<'_> { 214 | self.try_wait(Timeout::Infinite).unwrap(); 215 | SemaphoreGuard { semaphore: self } 216 | } 217 | 218 | #[cfg_attr(not(test), allow(unused))] 219 | fn wait0(&self) -> Result, rsevents::TimeoutError> { 220 | self.try_wait(Timeout::None)?; 221 | Ok(SemaphoreGuard { semaphore: self }) 222 | } 223 | 224 | /// Attempts a time-bounded wait against the `Semaphore`, returning `Ok(())` if and when the 225 | /// semaphore becomes available or a [`TimeoutError`](rsevents::TimeoutError) if the specified 226 | /// time limit elapses without the semaphore becoming available to the calling thread. 227 | #[must_use = "The semaphore count is immediately re-incremented if the guard is dropped"] 228 | pub fn wait_for(&self, limit: Duration) -> Result, rsevents::TimeoutError> { 229 | match limit { 230 | Duration::ZERO => self.try_wait(Timeout::None)?, 231 | timeout => self.try_wait(Timeout::Bounded(timeout))?, 232 | }; 233 | Ok(SemaphoreGuard { semaphore: self }) 234 | } 235 | 236 | #[inline] 237 | /// Directly increments the available concurrency count by `count`, without checking if this 238 | /// would violate the maximum available concurrency count. 239 | unsafe fn release_internal(&self, count: Count) { 240 | let prev_count = self.count.fetch_add(count, Ordering::Release); 241 | 242 | // We only need to set the AutoResetEvent if the count was previously exhausted. 243 | // In all other cases, the last thread to obtain the semaphore would have already set the 244 | // event (and auto-reset events saturate/clamp immediately). 245 | if prev_count == 0 { 246 | self.event.set(); 247 | } 248 | } 249 | 250 | /// Directly modifies the maximum currently available concurrency `current`, without regard for 251 | /// overflow or a violation of the semaphore's maximum allowed count. 252 | unsafe fn modify_current(&self, count: ICount) { 253 | match count.signum() { 254 | 0 => return, 255 | 1 => self.current.fetch_add(count as Count, Ordering::Relaxed), 256 | -1 => self 257 | .current 258 | .fetch_sub((count as INext).unsigned_abs() as Count, Ordering::Relaxed), 259 | _ => unsafe { core::hint::unreachable_unchecked() }, 260 | }; 261 | } 262 | 263 | /// Directly increments or decrements the current availability limit for a `Semaphore` without 264 | /// blocking. This is only possible when the semaphore is not currently borrowed or being waited 265 | /// on. Panics if the change will result in an available concurrency limit of less than zero or 266 | /// greater than the semaphore's maximum. See [`Semaphore::try_modify()`] for a non-panicking 267 | /// alternative. 268 | /// 269 | /// To increment the semaphore's concurrency limit without an `&mut Semaphore` reference, call 270 | /// [`Semaphore::release()`] instead. To decrement the concurrency limit, wait on the semaphore 271 | /// then call [`forget()`](SemaphoreGuard::forget) on the returned `SemaphoreGuard`: 272 | /// 273 | /// ```rust 274 | /// use rsevents_extra::Semaphore; 275 | /// 276 | /// fn adjust_sem(sem: &Semaphore, count: i16) { 277 | /// if count >= 0 { 278 | /// sem.release(count as u16); 279 | /// } else { 280 | /// // Note: this will block if the semaphore isn't available! 281 | /// for _ in 0..(-1 * count) { 282 | /// let guard = sem.wait(); 283 | /// guard.forget(); 284 | /// } 285 | /// } 286 | /// } 287 | /// ``` 288 | pub fn modify(&mut self, count: ICount) { 289 | let current = self.current.load(Ordering::Relaxed); 290 | match (current as INext).checked_add(count as INext) { 291 | Some(sum) if sum <= (self.max as INext) => {} 292 | _ => panic!("An invalid count was supplied to Semaphore::modify()"), 293 | }; 294 | 295 | match count.signum() { 296 | 0 => (), 297 | 1 => { 298 | self.current.fetch_add(count as Count, Ordering::Relaxed); 299 | self.count.fetch_add(count as Count, Ordering::Relaxed); 300 | } 301 | -1 => { 302 | self.current 303 | .fetch_add((count as INext).unsigned_abs() as Count, Ordering::Relaxed); 304 | self.count 305 | .fetch_add((count as INext).unsigned_abs() as Count, Ordering::Relaxed); 306 | } 307 | _ => unsafe { 308 | core::hint::unreachable_unchecked(); 309 | }, 310 | } 311 | } 312 | 313 | /// Directly increments or decrements the current availability limit for a `Semaphore` without 314 | /// blocking. This is only possible when the semaphore is not currently borrowed or being waited 315 | /// on. Returns `false` if the change will result in an available concurrency limit of less 316 | /// than zero or greater than the semaphore's maximum. 317 | /// 318 | /// See [`Semaphore::modify()`] for more info. 319 | pub fn try_modify(&mut self, count: ICount) -> bool { 320 | let current = self.current.load(Ordering::Relaxed); 321 | match (current as INext).checked_add(count as INext) { 322 | Some(sum) if sum <= (self.max as INext) => {} 323 | _ => return false, 324 | }; 325 | 326 | match count.signum() { 327 | 0 => return true, 328 | 1 => { 329 | self.current.fetch_add(count as Count, Ordering::Relaxed); 330 | self.count.fetch_add(count as Count, Ordering::Relaxed); 331 | } 332 | -1 => { 333 | self.current 334 | .fetch_add((count as INext).unsigned_abs() as Count, Ordering::Relaxed); 335 | self.count 336 | .fetch_add((count as INext).unsigned_abs() as Count, Ordering::Relaxed); 337 | } 338 | _ => unsafe { 339 | core::hint::unreachable_unchecked(); 340 | }, 341 | }; 342 | 343 | true 344 | } 345 | 346 | /// Increments the available concurrency by `count`, and panics if this results in a count that 347 | /// exceeds the `max_count` the `Semaphore` was created with (see [`Semaphore::new()`]). Unlike 348 | /// [`Semaphore::modify()`], this can be called with a non-mutable reference to the semaphore, 349 | /// but can only increment the concurrency level. 350 | /// 351 | /// See [`try_release`](Self::try_release) for a non-panicking version of this function. 352 | /// See the documentation for [`modify()`](Self::modify) for info on decrementing the available 353 | /// concurrency level. 354 | pub fn release(&self, count: Count) { 355 | // Increment the "current maximum" which includes borrowed semaphore instances. 356 | let prev_count = self.current.fetch_add(count, Ordering::Relaxed); 357 | match prev_count.checked_add(count) { 358 | Some(sum) if sum <= self.max => {} 359 | _ => panic!("Semaphore::release() called with an inappropriate count!"), 360 | } 361 | // Increment the actual "currently available" count to match. The two fields do not need to 362 | // be updated atomically because we only care that the previous operation succeeded, but do 363 | // not need to modify this variable contingent on that one. 364 | unsafe { 365 | self.release_internal(count); 366 | } 367 | } 368 | 369 | /// Attempts to increment the available concurrency counter by `count`, and returns `false` if 370 | /// this operation would result in a count that exceeds the `max_count` the `Semaphore` was 371 | /// created with (see [`Semaphore::new()`]). 372 | /// 373 | /// If you can guarantee that the count cannot exceed the maximum allowed, you may want to use 374 | /// [`Semaphore::release()`] instead as it is both lock-free and wait-free, whereas 375 | /// `try_release()` is only lock-free and may spin internally in case of contention. 376 | pub fn try_release(&self, count: Count) -> bool { 377 | // Try to increment the "current maximum" which includes borrowed semaphore instances. 378 | let mut prev_count = self.current.load(Ordering::Relaxed); 379 | loop { 380 | match prev_count.checked_add(count) { 381 | Some(sum) if sum <= self.max => {} 382 | _ => return false, 383 | } 384 | match self.current.compare_exchange_weak( 385 | prev_count, 386 | prev_count + count, 387 | Ordering::Relaxed, 388 | Ordering::Relaxed, 389 | ) { 390 | Ok(_) => break, 391 | Err(new_count) => prev_count = new_count, 392 | } 393 | } 394 | 395 | // Increment the actual "currently available" count to match. The two fields do not need to 396 | // be updated atomically because we only care that the previous operation succeeded, but do 397 | // not need to modify this variable contingent on that one. 398 | unsafe { 399 | self.release_internal(count); 400 | } 401 | 402 | true 403 | } 404 | 405 | /// Returns the currently available count of the semaphore. 406 | /// 407 | /// Note that this may race with other calls such as `release()` or `wait()`. 408 | pub fn count(&self) -> Count { 409 | self.count.load(Ordering::Relaxed) 410 | } 411 | } 412 | 413 | impl<'a> Awaitable<'a> for Semaphore { 414 | type T = SemaphoreGuard<'a>; 415 | type Error = TimeoutError; 416 | 417 | /// Attempts to obtain access to the resource or code protected by the `Semaphore`, subject to 418 | /// the available concurrency count. Returns immediately if the `Semaphore`'s internal 419 | /// concurrency count is non-zero or blocks sleeping until the `Semaphore` becomes available 420 | /// (via another thread completing its access to the controlled-concurrency region or if the 421 | /// semaphore's concurrency limit is raised). 422 | /// 423 | /// A successful wait against the semaphore decrements its internal available concurrency 424 | /// count (possibly preventing other threads from obtaining the semaphore) until 425 | /// [`Semaphore::release()`] is called. 426 | fn try_wait(&'a self) -> Result, Infallible> { 427 | self.try_wait(Timeout::Infinite).unwrap(); 428 | Ok(SemaphoreGuard { semaphore: self }) 429 | } 430 | 431 | /// Attempts a time-bounded wait against the `Semaphore`, returning `Ok(())` if and when the 432 | /// semaphore becomes available or a [`TimeoutError`](rsevents::TimeoutError) if the specified 433 | /// time limit elapses without the semaphore becoming available to the calling thread. 434 | fn try_wait_for( 435 | &'a self, 436 | limit: Duration, 437 | ) -> Result, rsevents::TimeoutError> { 438 | self.try_wait(Timeout::Bounded(limit))?; 439 | Ok(SemaphoreGuard { semaphore: self }) 440 | } 441 | 442 | /// Attempts to obtain the `Semaphore` without waiting, returning `Ok(())` if the semaphore 443 | /// is immediately available or a [`TimeoutError`](rsevents::TimeoutError) otherwise. 444 | fn try_wait0(&'a self) -> Result, rsevents::TimeoutError> { 445 | self.try_wait(Timeout::None)?; 446 | Ok(SemaphoreGuard { semaphore: self }) 447 | } 448 | } 449 | 450 | /// The concurrency token returned by [`Semaphore::wait()`], allowing access to the 451 | /// concurrency-limited region/code. Gives up its slot when dropped, allowing another thread to 452 | /// enter the semaphore in its place. 453 | /// 454 | /// `SemaphoreGuard` instances should never be passed to `std::mem::forget()` – 455 | /// [`SemaphoreGuard::forget()`] should be called instead to forget a `SemaphoreGuard` and 456 | /// permanently decrease the available concurrency. 457 | pub struct SemaphoreGuard<'a> { 458 | semaphore: &'a Semaphore, 459 | } 460 | 461 | impl SemaphoreGuard<'_> { 462 | /// Safely "forgets" a semaphore's guard, permanently reducing the concurrency limit of the 463 | /// associated `Semaphore`. `SemaphoreGuard::forget()` internally decrements the semaphore's 464 | /// availablibility counter to make sure that future calls to `Semaphore::release()` or 465 | /// `Semaphore::try_release()` do not incorrectly report failure. 466 | /// 467 | /// A `SemaphoreGuard` instance should never be passed to `std::mem::forget()` directly, as that 468 | /// would violate the internal contract; this method should be used instead. 469 | pub fn forget(self) { 470 | unsafe { 471 | self.semaphore.modify_current(-1); 472 | } 473 | core::mem::forget(self); 474 | } 475 | } 476 | 477 | impl Debug for SemaphoreGuard<'_> { 478 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 479 | f.debug_struct("SemaphoreGuard").finish_non_exhaustive() 480 | } 481 | } 482 | 483 | impl Drop for SemaphoreGuard<'_> { 484 | fn drop(&mut self) { 485 | unsafe { 486 | self.semaphore.release_internal(1); 487 | } 488 | } 489 | } 490 | 491 | #[cfg(test)] 492 | mod test { 493 | use super::Count; 494 | use crate::Semaphore; 495 | use rsevents::Awaitable; 496 | use std::thread; 497 | use std::time::Duration; 498 | 499 | #[test] 500 | fn uncontested_semaphore() { 501 | let sem = Semaphore::new(1, 1); 502 | let _1 = sem.wait0().unwrap(); 503 | sem.try_wait0().unwrap_err(); 504 | } 505 | 506 | #[test] 507 | fn zero_semaphore() { 508 | let sem = Semaphore::new(0, 0); 509 | sem.try_wait0().unwrap_err(); 510 | } 511 | 512 | fn release_x_of_y_sequentially(x: Count, y: Count) -> Semaphore { 513 | let sem: Semaphore = Semaphore::new(0, y); 514 | 515 | // Use thread::scope because it automatically joins all threads, 516 | // which is useful if they panic. 517 | thread::scope(|scope| { 518 | for _ in 0..x { 519 | scope.spawn(|| { 520 | sem.wait0().unwrap_err(); 521 | let lock = sem.wait_for(Duration::from_secs(1)).unwrap(); 522 | // Correct way to "forget" a semaphore slot; never pass a 523 | // SemaphoreGuard to std::mem::forget()! 524 | lock.forget(); 525 | }); 526 | } 527 | 528 | scope.spawn(|| { 529 | std::thread::sleep(Duration::from_millis(100)); 530 | for _ in 0..x { 531 | sem.release(1); 532 | } 533 | }); 534 | }); 535 | 536 | sem 537 | } 538 | 539 | fn release_x_of_y(x: Count, y: Count) -> Semaphore { 540 | let sem: Semaphore = Semaphore::new(0, y); 541 | 542 | // Use thread::scope because it automatically joins all threads, 543 | // which is useful if they panic. 544 | thread::scope(|scope| { 545 | for _ in 0..x { 546 | scope.spawn(|| { 547 | sem.wait0().unwrap_err(); 548 | let lock = sem.wait_for(Duration::from_secs(1)).unwrap(); 549 | std::mem::forget(lock); 550 | }); 551 | } 552 | 553 | scope.spawn(|| { 554 | std::thread::sleep(Duration::from_millis(100)); 555 | sem.release(x); 556 | }); 557 | }); 558 | 559 | sem 560 | } 561 | 562 | #[test] 563 | fn release_1_of_1() { 564 | release_x_of_y(1, 1); 565 | } 566 | 567 | #[test] 568 | fn release_1_of_2() { 569 | release_x_of_y(1, 2); 570 | } 571 | 572 | #[test] 573 | fn release_2_of_3() { 574 | release_x_of_y(1, 2); 575 | } 576 | 577 | #[test] 578 | fn release_2_of_2() { 579 | let sem = release_x_of_y_sequentially(2, 2); 580 | sem.wait0().unwrap_err(); 581 | } 582 | } 583 | --------------------------------------------------------------------------------