Skip to content

Commit dbfbb3e

Browse files
committed
feat: Add no_std implementation
This commit adds an implementation of the event-listener algorithm built without locks. This is a replacement of the old no_std backend. It is written without concurrent-queue and therefore closes #109. The idea behind this implementation is to store the listeners in "slots" in an infinitely large list. Then, assign each listener a number and then use that to wait on each listener. The list used is similar to the one used by the thread-local crate. It consists of a list of "buckets" that hold slots. The number of slots increases in an amortized way. The first slot holds 1, the second slot holds 2, the third slot holds 4... all the way up to usize::MAX slots. Indexes are done by having a list of reusable indexes and using those when possible, only increasing the max index when necessary. This part of the code could use some work; under contention it's possible to make some slots unusuable. This can happen under two cases: - If there is contention on the indexes list when the listener is being freed. - If the slot is still being notified when it is attempted to be reused. Both of these cases are probably fixable, and should be fixed before release. Otherwise long running server processes using this code will run out of memory under heavy loads. From here the rest of the implementation is an atomic linked list based on the above primitives. It functions very similarly to the std variant. The main difference is that the Link structure's waker functions very similarly to AtomicWaker from the atomic-waker crate. Aside from that the code isn't very interesting on its own. Signed-off-by: John Nunley <[email protected]>
1 parent 19ef495 commit dbfbb3e

File tree

9 files changed

+1061
-203
lines changed

9 files changed

+1061
-203
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ jobs:
3636
- uses: actions/checkout@v4
3737
- name: Install Rust
3838
run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }}
39+
- run: cargo build --all --all-targets
40+
- run: cargo build --all --no-default-features --all-targets
3941
- run: cargo build --all --all-features --all-targets
4042
- name: Run cargo check (without dev-dependencies to catch missing feature flags)
4143
if: startsWith(matrix.rust, 'nightly')
@@ -48,12 +50,13 @@ jobs:
4850
matrix:
4951
# When updating this, the reminder to update the minimum supported
5052
# Rust version in Cargo.toml.
51-
rust: ['1.39']
53+
rust: ['1.59']
5254
steps:
5355
- uses: actions/checkout@v4
5456
- name: Install Rust
5557
run: rustup update ${{ matrix.rust }} && rustup default ${{ matrix.rust }}
5658
- run: cargo build
59+
- run: cargo build --no-default-features
5760

5861
clippy:
5962
runs-on: ubuntu-latest
@@ -62,6 +65,8 @@ jobs:
6265
- name: Install Rust
6366
run: rustup update stable
6467
- run: cargo clippy --all-features --all-targets
68+
- run: cargo clippy --all-targets
69+
- run: cargo clippy --no-default-features --all-targets
6570

6671
fmt:
6772
runs-on: ubuntu-latest

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ name = "event-listener"
66
version = "2.5.3"
77
authors = ["Stjepan Glavina <[email protected]>"]
88
edition = "2021"
9-
rust-version = "1.56"
9+
rust-version = "1.59"
1010
description = "Notify async tasks or threads"
1111
license = "Apache-2.0 OR MIT"
1212
repository = "https://github.com/smol-rs/event-listener"
@@ -21,9 +21,8 @@ portable-atomic = ["portable-atomic-crate", "portable-atomic-util"]
2121

2222
[dependencies]
2323
portable-atomic-crate = { version = "1.6.0", package = "portable-atomic", optional = true }
24-
portable-atomic-util = { version = "0.1.5", optional = true }
24+
portable-atomic-util = { version = "0.1.5", features = ["alloc"], optional = true }
2525

2626
[dev-dependencies]
27-
futures = { version = "0.3", default-features = false, features = ["std"] }
2827
futures-lite = "2.3.0"
2928
waker-fn = "1"

examples/mutex.rs

Lines changed: 147 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -4,182 +4,194 @@
44
55
#![allow(dead_code)]
66

7-
use std::cell::UnsafeCell;
8-
use std::ops::{Deref, DerefMut};
9-
use std::sync::atomic::{AtomicBool, Ordering};
10-
use std::sync::{mpsc, Arc};
11-
use std::thread;
12-
use std::time::{Duration, Instant};
13-
14-
use event_listener::{Event, Listener};
15-
16-
/// A simple mutex.
17-
struct Mutex<T> {
18-
/// Set to `true` when the mutex is locked.
19-
locked: AtomicBool,
20-
21-
/// Blocked lock operations.
22-
lock_ops: Event,
23-
24-
/// The inner protected data.
25-
data: UnsafeCell<T>,
26-
}
7+
#[cfg(feature = "std")]
8+
mod ex {
9+
use std::cell::UnsafeCell;
10+
use std::ops::{Deref, DerefMut};
11+
use std::sync::atomic::{AtomicBool, Ordering};
12+
use std::sync::{mpsc, Arc};
13+
use std::thread;
14+
use std::time::{Duration, Instant};
15+
16+
use event_listener::{Event, Listener};
17+
18+
/// A simple mutex.
19+
struct Mutex<T> {
20+
/// Set to `true` when the mutex is locked.
21+
locked: AtomicBool,
22+
23+
/// Blocked lock operations.
24+
lock_ops: Event,
25+
26+
/// The inner protected data.
27+
data: UnsafeCell<T>,
28+
}
2729

28-
unsafe impl<T: Send> Send for Mutex<T> {}
29-
unsafe impl<T: Send> Sync for Mutex<T> {}
30+
unsafe impl<T: Send> Send for Mutex<T> {}
31+
unsafe impl<T: Send> Sync for Mutex<T> {}
3032

31-
impl<T> Mutex<T> {
32-
/// Creates a mutex.
33-
fn new(t: T) -> Mutex<T> {
34-
Mutex {
35-
locked: AtomicBool::new(false),
36-
lock_ops: Event::new(),
37-
data: UnsafeCell::new(t),
33+
impl<T> Mutex<T> {
34+
/// Creates a mutex.
35+
fn new(t: T) -> Mutex<T> {
36+
Mutex {
37+
locked: AtomicBool::new(false),
38+
lock_ops: Event::new(),
39+
data: UnsafeCell::new(t),
40+
}
3841
}
39-
}
4042

41-
/// Attempts to acquire a lock.
42-
fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
43-
if !self.locked.swap(true, Ordering::Acquire) {
44-
Some(MutexGuard(self))
45-
} else {
46-
None
43+
/// Attempts to acquire a lock.
44+
fn try_lock(&self) -> Option<MutexGuard<'_, T>> {
45+
if !self.locked.swap(true, Ordering::Acquire) {
46+
Some(MutexGuard(self))
47+
} else {
48+
None
49+
}
4750
}
48-
}
49-
50-
/// Blocks until a lock is acquired.
51-
fn lock(&self) -> MutexGuard<'_, T> {
52-
let mut listener = None;
5351

54-
loop {
55-
// Attempt grabbing a lock.
56-
if let Some(guard) = self.try_lock() {
57-
return guard;
58-
}
52+
/// Blocks until a lock is acquired.
53+
fn lock(&self) -> MutexGuard<'_, T> {
54+
let mut listener = None;
5955

60-
// Set up an event listener or wait for a notification.
61-
match listener.take() {
62-
None => {
63-
// Start listening and then try locking again.
64-
listener = Some(self.lock_ops.listen());
56+
loop {
57+
// Attempt grabbing a lock.
58+
if let Some(guard) = self.try_lock() {
59+
return guard;
6560
}
66-
Some(l) => {
67-
// Wait until a notification is received.
68-
l.wait();
61+
62+
// Set up an event listener or wait for a notification.
63+
match listener.take() {
64+
None => {
65+
// Start listening and then try locking again.
66+
listener = Some(self.lock_ops.listen());
67+
}
68+
Some(l) => {
69+
// Wait until a notification is received.
70+
l.wait();
71+
}
6972
}
7073
}
7174
}
72-
}
73-
74-
/// Blocks until a lock is acquired or the timeout is reached.
75-
fn lock_timeout(&self, timeout: Duration) -> Option<MutexGuard<'_, T>> {
76-
let deadline = Instant::now() + timeout;
77-
let mut listener = None;
7875

79-
loop {
80-
// Attempt grabbing a lock.
81-
if let Some(guard) = self.try_lock() {
82-
return Some(guard);
83-
}
76+
/// Blocks until a lock is acquired or the timeout is reached.
77+
fn lock_timeout(&self, timeout: Duration) -> Option<MutexGuard<'_, T>> {
78+
let deadline = Instant::now() + timeout;
79+
let mut listener = None;
8480

85-
// Set up an event listener or wait for an event.
86-
match listener.take() {
87-
None => {
88-
// Start listening and then try locking again.
89-
listener = Some(self.lock_ops.listen());
81+
loop {
82+
// Attempt grabbing a lock.
83+
if let Some(guard) = self.try_lock() {
84+
return Some(guard);
9085
}
91-
Some(l) => {
92-
// Wait until a notification is received.
93-
if l.wait_deadline(deadline).is_none() {
94-
return None;
86+
87+
// Set up an event listener or wait for an event.
88+
match listener.take() {
89+
None => {
90+
// Start listening and then try locking again.
91+
listener = Some(self.lock_ops.listen());
92+
}
93+
Some(l) => {
94+
// Wait until a notification is received.
95+
l.wait_deadline(deadline)?;
9596
}
9697
}
9798
}
9899
}
99-
}
100100

101-
/// Acquires a lock asynchronously.
102-
async fn lock_async(&self) -> MutexGuard<'_, T> {
103-
let mut listener = None;
104-
105-
loop {
106-
// Attempt grabbing a lock.
107-
if let Some(guard) = self.try_lock() {
108-
return guard;
109-
}
101+
/// Acquires a lock asynchronously.
102+
async fn lock_async(&self) -> MutexGuard<'_, T> {
103+
let mut listener = None;
110104

111-
// Set up an event listener or wait for an event.
112-
match listener.take() {
113-
None => {
114-
// Start listening and then try locking again.
115-
listener = Some(self.lock_ops.listen());
105+
loop {
106+
// Attempt grabbing a lock.
107+
if let Some(guard) = self.try_lock() {
108+
return guard;
116109
}
117-
Some(l) => {
118-
// Wait until a notification is received.
119-
l.await;
110+
111+
// Set up an event listener or wait for an event.
112+
match listener.take() {
113+
None => {
114+
// Start listening and then try locking again.
115+
listener = Some(self.lock_ops.listen());
116+
}
117+
Some(l) => {
118+
// Wait until a notification is received.
119+
l.await;
120+
}
120121
}
121122
}
122123
}
123124
}
124-
}
125125

126-
/// A guard holding a lock.
127-
struct MutexGuard<'a, T>(&'a Mutex<T>);
126+
/// A guard holding a lock.
127+
struct MutexGuard<'a, T>(&'a Mutex<T>);
128128

129-
unsafe impl<T: Send> Send for MutexGuard<'_, T> {}
130-
unsafe impl<T: Sync> Sync for MutexGuard<'_, T> {}
129+
unsafe impl<T: Send> Send for MutexGuard<'_, T> {}
130+
unsafe impl<T: Sync> Sync for MutexGuard<'_, T> {}
131131

132-
impl<T> Drop for MutexGuard<'_, T> {
133-
fn drop(&mut self) {
134-
self.0.locked.store(false, Ordering::Release);
135-
self.0.lock_ops.notify(1);
132+
impl<T> Drop for MutexGuard<'_, T> {
133+
fn drop(&mut self) {
134+
self.0.locked.store(false, Ordering::Release);
135+
self.0.lock_ops.notify(1);
136+
}
136137
}
137-
}
138138

139-
impl<T> Deref for MutexGuard<'_, T> {
140-
type Target = T;
139+
impl<T> Deref for MutexGuard<'_, T> {
140+
type Target = T;
141141

142-
fn deref(&self) -> &T {
143-
unsafe { &*self.0.data.get() }
142+
fn deref(&self) -> &T {
143+
unsafe { &*self.0.data.get() }
144+
}
144145
}
145-
}
146146

147-
impl<T> DerefMut for MutexGuard<'_, T> {
148-
fn deref_mut(&mut self) -> &mut T {
149-
unsafe { &mut *self.0.data.get() }
147+
impl<T> DerefMut for MutexGuard<'_, T> {
148+
fn deref_mut(&mut self) -> &mut T {
149+
unsafe { &mut *self.0.data.get() }
150+
}
150151
}
151-
}
152152

153-
fn main() {
154-
const N: usize = 10;
153+
pub(crate) fn entry() {
154+
const N: usize = 10;
155155

156-
// A shared counter.
157-
let counter = Arc::new(Mutex::new(0));
156+
// A shared counter.
157+
let counter = Arc::new(Mutex::new(0));
158158

159-
// A channel that signals when all threads are done.
160-
let (tx, rx) = mpsc::channel();
159+
// A channel that signals when all threads are done.
160+
let (tx, rx) = mpsc::channel();
161161

162-
// Spawn a bunch of threads incrementing the counter.
163-
for _ in 0..N {
164-
let counter = counter.clone();
165-
let tx = tx.clone();
162+
// Spawn a bunch of threads incrementing the counter.
163+
for _ in 0..N {
164+
let counter = counter.clone();
165+
let tx = tx.clone();
166166

167-
thread::spawn(move || {
168-
let mut counter = counter.lock();
169-
*counter += 1;
167+
thread::spawn(move || {
168+
let mut counter = counter.lock();
169+
*counter += 1;
170170

171-
// If this is the last increment, signal that we're done.
172-
if *counter == N {
173-
tx.send(()).unwrap();
174-
}
175-
});
176-
}
171+
// If this is the last increment, signal that we're done.
172+
if *counter == N {
173+
tx.send(()).unwrap();
174+
}
175+
});
176+
}
177177

178-
// Wait until the last thread increments the counter.
179-
rx.recv().unwrap();
178+
// Wait until the last thread increments the counter.
179+
rx.recv().unwrap();
180180

181-
// The counter must equal the number of threads.
182-
assert_eq!(*counter.lock(), N);
181+
// The counter must equal the number of threads.
182+
assert_eq!(*counter.lock(), N);
183183

184-
println!("Done!");
184+
println!("Done!");
185+
}
186+
}
187+
188+
#[cfg(not(feature = "std"))]
189+
mod ex {
190+
pub(crate) fn entry() {
191+
eprintln!("this example requires the 'std' feature")
192+
}
193+
}
194+
195+
fn main() {
196+
ex::entry();
185197
}

0 commit comments

Comments
 (0)