Skip to content

Commit 2f1fa5e

Browse files
authored
refactor: clarify waiter arena contracts (#191)
1 parent cc4db52 commit 2f1fa5e

9 files changed

Lines changed: 209 additions & 154 deletions

File tree

asyncband/src/barrier/mod.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ use std::task::Context;
7777
use std::task::Poll;
7878

7979
use crate::internal::mutex::Mutex;
80-
use crate::internal::waitset::WaitRegistration;
8180
use crate::internal::waitset::WaitSet;
81+
use crate::internal::waitset::WakerToken;
8282

8383
/// A synchronization primitive for multiple tasks that need to wait for each other.
8484
///
@@ -255,7 +255,7 @@ impl Barrier {
255255
};
256256

257257
let fut = BarrierWait {
258-
registration: None,
258+
token: None,
259259
generation,
260260
barrier: self,
261261
};
@@ -269,7 +269,7 @@ impl Barrier {
269269
/// This future will complete when all tasks have reached the barrier point.
270270
#[must_use = "futures do nothing unless you `.await` or poll them"]
271271
struct BarrierWait<'a> {
272-
registration: Option<WaitRegistration>,
272+
token: Option<WakerToken>,
273273
generation: usize,
274274
barrier: &'a Barrier,
275275
}
@@ -287,7 +287,7 @@ impl Future for BarrierWait<'_> {
287287

288288
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
289289
let Self {
290-
registration,
290+
token,
291291
generation,
292292
barrier,
293293
} = self.get_mut();
@@ -296,10 +296,10 @@ impl Future for BarrierWait<'_> {
296296
let mut state = barrier.state.lock();
297297
if *generation < state.generation {
298298
// Advancing the generation drains its registrations under this same lock.
299-
*registration = None;
299+
*token = None;
300300
return Poll::Ready(());
301301
}
302-
state.waiters.register_waker(registration, cx)
302+
state.waiters.register_waker(token, cx)
303303
};
304304
drop(replaced_waker);
305305
Poll::Pending
@@ -308,10 +308,10 @@ impl Future for BarrierWait<'_> {
308308

309309
impl Drop for BarrierWait<'_> {
310310
fn drop(&mut self) {
311-
if self.registration.is_some() {
311+
if self.token.is_some() {
312312
let removed_waker = {
313313
let mut state = self.barrier.state.lock();
314-
state.waiters.unregister_waker(&mut self.registration)
314+
state.waiters.unregister_waker(&mut self.token)
315315
};
316316
drop(removed_waker);
317317
}

asyncband/src/internal/arena.rs

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,39 @@
1818
use std::mem;
1919
use std::num::NonZeroUsize;
2020

21-
/// A stable index into an [`Arena`] for as long as its slot remains occupied.
22-
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23-
pub struct ArenaKey(usize);
24-
25-
impl ArenaKey {
26-
/// Encodes this key so it can provide a niche when stored in an `Option`-wrapped structure.
27-
pub fn encode(self) -> NonZeroUsize {
21+
/// Identifies a reusable slot while that slot is occupied.
22+
///
23+
/// The non-zero representation lets wrappers such as `WaiterId` retain a niche when stored in an
24+
/// `Option`. Slot IDs deliberately carry no generation: each consumer supplies the cheaper
25+
/// lifecycle rule that matches its waiter storage.
26+
#[derive(Clone, Copy, Eq, PartialEq)]
27+
pub struct SlotId(NonZeroUsize);
28+
29+
impl SlotId {
30+
fn from_index(index: usize) -> Self {
2831
// `Slot<T>` is non-zero-sized, so a Vec of slots cannot reach `usize::MAX` elements.
29-
unsafe { NonZeroUsize::new_unchecked(self.0 + 1) }
32+
let encoded = index
33+
.checked_add(1)
34+
.expect("arena index must fit in a non-zero usize");
35+
Self(NonZeroUsize::new(encoded).expect("encoded arena index must be non-zero"))
36+
}
37+
38+
fn index(self) -> usize {
39+
self.0.get() - 1
3040
}
41+
}
3142

32-
/// Decodes a key produced by [`Self::encode`].
33-
pub fn decode(encoded: NonZeroUsize) -> Self {
34-
Self(encoded.get() - 1)
43+
impl std::fmt::Debug for SlotId {
44+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45+
f.debug_tuple("SlotId").field(&self.index()).finish()
3546
}
3647
}
3748

3849
/// Minimal reusable storage for internal waiter state.
50+
///
51+
/// The occupied length equals the number of `Occupied` slots. Every `Vacant` slot appears exactly
52+
/// once in the singly linked vacant list, which starts at `next_vacant` and terminates at
53+
/// `slots.len()`. Removing a value makes its slot ID available for immediate reuse.
3954
#[derive(Debug)]
4055
pub struct Arena<T> {
4156
slots: Vec<Slot<T>>,
@@ -45,8 +60,12 @@ pub struct Arena<T> {
4560
}
4661

4762
/// Values extracted from an [`Arena`], storing the common single-value case inline.
63+
///
64+
/// This is the specialized subset of a small-vector abstraction needed here: keep one value inline,
65+
/// store additional values in a `Vec`, and support consuming iteration. Keeping that representation
66+
/// focused avoids a general unsafe collection implementation for a single internal operation.
4867
#[derive(Debug)]
49-
pub struct ArenaValues<T> {
68+
struct ArenaValues<T> {
5069
first: Option<T>,
5170
rest: Vec<T>,
5271
}
@@ -63,7 +82,7 @@ impl<T> IntoIterator for ArenaValues<T> {
6382
#[derive(Debug)]
6483
enum Slot<T> {
6584
Occupied(T),
66-
Vacant(usize),
85+
Vacant { next: usize },
6786
}
6887

6988
impl<T> Arena<T> {
@@ -83,61 +102,76 @@ impl<T> Arena<T> {
83102
}
84103
}
85104

86-
pub fn insert(&mut self, value: T) -> ArenaKey {
87-
let key = self.next_vacant;
105+
pub fn insert(&mut self, value: T) -> SlotId {
106+
let index = self.next_vacant;
88107
self.len += 1;
89108

90-
if key == self.slots.len() {
109+
if index == self.slots.len() {
91110
self.slots.push(Slot::Occupied(value));
92-
self.next_vacant = key + 1;
111+
self.next_vacant = index + 1;
93112
} else {
94-
self.next_vacant = match self.slots.get(key) {
95-
Some(Slot::Vacant(next)) => *next,
113+
self.next_vacant = match self.slots.get(index) {
114+
Some(Slot::Vacant { next }) => *next,
96115
Some(Slot::Occupied(_)) | None => {
97116
unreachable!("arena free list must point to a vacant slot")
98117
}
99118
};
100-
self.slots[key] = Slot::Occupied(value);
119+
self.slots[index] = Slot::Occupied(value);
101120
}
102121

103-
ArenaKey(key)
122+
SlotId::from_index(index)
104123
}
105124

106-
pub fn get(&self, key: ArenaKey) -> Option<&T> {
107-
match self.slots.get(key.0) {
125+
pub fn get(&self, id: SlotId) -> Option<&T> {
126+
match self.slots.get(id.index()) {
108127
Some(Slot::Occupied(value)) => Some(value),
109-
Some(Slot::Vacant(_)) | None => None,
128+
Some(Slot::Vacant { .. }) | None => None,
110129
}
111130
}
112131

113-
pub fn get_mut(&mut self, key: ArenaKey) -> Option<&mut T> {
114-
match self.slots.get_mut(key.0) {
132+
pub fn get_mut(&mut self, id: SlotId) -> Option<&mut T> {
133+
match self.slots.get_mut(id.index()) {
115134
Some(Slot::Occupied(value)) => Some(value),
116-
Some(Slot::Vacant(_)) | None => None,
135+
Some(Slot::Vacant { .. }) | None => None,
117136
}
118137
}
119138

120-
pub fn remove(&mut self, key: ArenaKey) -> T {
121-
let index = key.0;
139+
/// Removes the value stored at `id`.
140+
///
141+
/// # Panics
142+
///
143+
/// Panics if the slot ID is out of bounds or its slot is already vacant. Either case is an
144+
/// internal waiter-lifecycle violation rather than a recoverable lookup failure.
145+
#[track_caller]
146+
pub fn remove(&mut self, id: SlotId) -> T {
147+
let index = id.index();
122148
let slot = self
123149
.slots
124150
.get_mut(index)
125-
.expect("arena key must be in bounds");
126-
let value = match mem::replace(slot, Slot::Vacant(self.next_vacant)) {
151+
.expect("arena slot ID must be in bounds");
152+
let value = match mem::replace(
153+
slot,
154+
Slot::Vacant {
155+
next: self.next_vacant,
156+
},
157+
) {
127158
Slot::Occupied(value) => value,
128-
vacant @ Slot::Vacant(_) => {
159+
vacant @ Slot::Vacant { .. } => {
129160
*slot = vacant;
130-
panic!("arena key must be occupied");
161+
panic!("arena slot ID must be occupied");
131162
}
132163
};
133164
self.len -= 1;
134165
self.next_vacant = index;
135166
value
136167
}
137168

138-
/// Takes every occupied value while retaining the allocation for reuse.
169+
/// Takes every occupied value in slot order while retaining the allocation for reuse.
170+
///
171+
/// Every previously issued slot ID becomes invalid, including IDs for slots that were already
172+
/// vacant. Consumers that retain IDs across this operation must supply their own epoch check.
139173
#[inline]
140-
pub fn take_all(&mut self) -> ArenaValues<T> {
174+
pub fn take_all(&mut self) -> impl Iterator<Item = T> + use<T> {
141175
let len = self.len;
142176
let mut values = ArenaValues {
143177
first: None,
@@ -158,7 +192,7 @@ impl<T> Arena<T> {
158192

159193
self.next_vacant = 0;
160194
self.len = 0;
161-
values
195+
values.into_iter()
162196
}
163197

164198
#[cfg(test)]
@@ -171,6 +205,11 @@ impl<T> Arena<T> {
171205
mod tests {
172206
use super::*;
173207

208+
#[test]
209+
fn slot_id_preserves_the_option_niche() {
210+
assert_eq!(size_of::<SlotId>(), size_of::<Option<SlotId>>());
211+
}
212+
174213
#[test]
175214
fn removed_slots_are_reused() {
176215
let mut arena = Arena::new();
@@ -186,19 +225,19 @@ mod tests {
186225
}
187226

188227
#[test]
189-
fn take_all_restarts_key_allocation() {
228+
fn take_all_restarts_slot_id_allocation() {
190229
let mut arena = Arena::with_capacity(3);
191230
let first = arena.insert(1);
192231
let second = arena.insert(2);
193232
let third = arena.insert(3);
194233
let capacity = arena.slots.capacity();
195234
arena.remove(second);
196235

197-
assert_eq!(arena.take_all().into_iter().collect::<Vec<_>>(), vec![1, 3]);
236+
assert_eq!(arena.take_all().collect::<Vec<_>>(), vec![1, 3]);
198237
assert_eq!(arena.len(), 0);
199238
assert_eq!(arena.slots.capacity(), capacity);
200239

201-
let keys = [arena.insert(4), arena.insert(5), arena.insert(6)];
202-
assert_eq!(keys, [first, second, third]);
240+
let slot_ids = [arena.insert(4), arena.insert(5), arena.insert(6)];
241+
assert_eq!(slot_ids, [first, second, third]);
203242
}
204243
}

asyncband/src/internal/countdown.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use std::task::Context;
2121
use std::task::Poll;
2222

2323
use crate::internal::mutex::Mutex;
24-
use crate::internal::waitset::WaitRegistration;
2524
use crate::internal::waitset::WaitSet;
25+
use crate::internal::waitset::WakerToken;
2626

2727
#[derive(Debug)]
2828
pub struct CountdownState {
@@ -72,37 +72,33 @@ impl CountdownState {
7272
}
7373

7474
/// Polls for zero, registering the current waker if the countdown is still active.
75-
pub fn poll_wait(
76-
&self,
77-
registration: &mut Option<WaitRegistration>,
78-
cx: &mut Context<'_>,
79-
) -> Poll<()> {
75+
pub fn poll_wait(&self, token: &mut Option<WakerToken>, cx: &mut Context<'_>) -> Poll<()> {
8076
if self.spin_wait(16).is_ok() {
8177
// The zero transition owns draining this wake epoch. Avoid taking the waiter lock
8278
// again when the completed future is dropped.
83-
*registration = None;
79+
*token = None;
8480
return Poll::Ready(());
8581
}
8682

8783
let replaced_waker = {
8884
let mut waiters = self.waiters.lock();
8985
if self.state() == 0 {
9086
// A concurrent zero transition will drain after this lock is released.
91-
*registration = None;
87+
*token = None;
9288
return Poll::Ready(());
9389
}
94-
waiters.register_waker(registration, cx)
90+
waiters.register_waker(token, cx)
9591
};
9692
drop(replaced_waker);
9793
Poll::Pending
9894
}
9995

10096
#[inline]
101-
pub fn unregister_waker(&self, registration: &mut Option<WaitRegistration>) {
102-
if registration.is_some() {
97+
pub fn unregister_waker(&self, token: &mut Option<WakerToken>) {
98+
if token.is_some() {
10399
let removed_waker = {
104100
let mut waiters = self.waiters.lock();
105-
waiters.unregister_waker(registration)
101+
waiters.unregister_waker(token)
106102
};
107103
drop(removed_waker);
108104
}

asyncband/src/internal/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub(crate) mod atomic_waker;
3030
// `WaitList` and `WaitSet` use different `Arena` operations. A single-primitive build therefore
3131
// leaves part of this shared API unused, while the all-feature build uses it.
3232
#[allow(dead_code)]
33-
pub(crate) mod arena;
33+
mod arena;
3434

3535
#[cfg(any(feature = "latch", feature = "once", feature = "waitgroup"))]
3636
// `waitgroup` increments and decrements the countdown, while `latch` and `once` only decrement it.

0 commit comments

Comments
 (0)