-
-
Notifications
You must be signed in to change notification settings - Fork 12
chore: use Vec instead of Slab to store waiting wakers in WaitSet #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
chore: use Vec instead of Slab to store waiting wakers in WaitSet #103
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR attempts to replace Slab<Waker> with Vec<Waker> for storing wakers in the WaitSet structure. The rationale is that WaitSet only appends wakers and drains them all at once, making Slab's ability to remove individual entries unnecessary.
Key changes:
- Replace
Slab<Waker>withVec<Waker>for the waiters field - Replace
Slab::contains(key)check withkey < self.waiters.len() - Update drain call from
drain()todrain(..)for Vec semantics
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if key < self.waiters.len() { | ||
| if !self.waiters[key].will_wake(cx.waker()) { | ||
| self.waiters[key] = cx.waker().clone(); | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This index validation check is incorrect and can lead to waker corruption. After wake_all() drains the Vec, the indices from previous registrations can collide with newly registered wakers.
Scenario demonstrating the bug:
- Task A registers and gets idx=0, Task B registers and gets idx=1
wake_all()is called, draining the Vec (now empty)- Task C registers for the first time and gets idx=0
- Task A is re-polled with its old idx=0
- The check
0 < self.waiters.len()(which is 1) passes - Task A's waker overwrites Task C's waker at index 0
- Task C will never be woken up
With Slab, the contains(key) method would correctly return false for indices from drained entries, preventing this collision. The simple length check cannot distinguish between "new index 0 after drain" and "old index 0 from before drain".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Slab has the same issue @copilot
let mut slab = Slab::<i32>::with_capacity(3);
let idx0 = slab.insert(1);
println!("Inserted 1 at index {}", idx0);
let idx1 = slab.insert(2);
println!("Inserted 2 at index {}", idx1);
slab.drain().for_each(|v| {
println!("Drained value: {}", v);
});
let idx2 = slab.insert(3);
println!("Inserted 3 at index {}", idx2);
let cached = slab.contains(idx0);
println!("Slab contains index {}: {}", idx0, cached);Outputs:
running 1 test
Inserted 1 at index 0
Inserted 2 at index 1
Drained value: 1
Drained value: 2
Inserted 3 at index 0
Slab contains index 0: true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah .. The index is actually a pointer without safety check. It would be better if we can build some dependent type check or more obvious invariants.
|
I tend to hold on this change until we do #102 so it has a good point to exclude the Currently, slab does no harm compare with vec also, IMO. |
| /// Drain and wake up all waiters. | ||
| pub(crate) fn wake_all(&mut self) { | ||
| for w in self.waiters.drain() { | ||
| for w in self.waiters.drain(..) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: when take the whole range of a vec, the following code should be more efficient
| for w in self.waiters.drain(..) { | |
| for w in std::mem::take(&mut self.waiters) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wake_all could be called many times, std::mem::take just replaces the Vec with an empty one so that the capacity is lost.
BTW, I noticed the capacity is only set in Barrier::new. Is this necessary?
Agreed. Adding features could be good to separate some unnecessary dependencies.
From the perspective of performance, it's true. Comparing to So it's just a chore that makes code more reasonable. |
Unlike
WaitList,WaitSetmanages wakers in a more simple way:register_wakerfor the 1st time to poll or the waker does not exist (due to a race)wake_allIn this case,
Slabhas no benefit.