Skip to content

Conversation

@BewareMyPower
Copy link
Contributor

Unlike WaitList, WaitSet manages wakers in a more simple way:

  • Append the waker in register_waker for the 1st time to poll or the waker does not exist (due to a race)
  • Drain all wakers in wake_all

In this case, Slab has no benefit.

Copy link

Copilot AI left a 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> with Vec<Waker> for the waiters field
  • Replace Slab::contains(key) check with key < self.waiters.len()
  • Update drain call from drain() to drain(..) for Vec semantics

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to 59
if key < self.waiters.len() {
if !self.waiters[key].will_wake(cx.waker()) {
self.waiters[key] = cx.waker().clone();
}
Copy link

Copilot AI Jan 6, 2026

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:

  1. Task A registers and gets idx=0, Task B registers and gets idx=1
  2. wake_all() is called, draining the Vec (now empty)
  3. Task C registers for the first time and gets idx=0
  4. Task A is re-polled with its old idx=0
  5. The check 0 < self.waiters.len() (which is 1) passes
  6. Task A's waker overwrites Task C's waker at index 0
  7. 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".

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

Copy link
Collaborator

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.

@tisonkun
Copy link
Collaborator

tisonkun commented Jan 7, 2026

I tend to hold on this change until we do #102 so it has a good point to exclude the slab dep when unnecessary.

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(..) {
Copy link
Collaborator

@tisonkun tisonkun Jan 7, 2026

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

Suggested change
for w in self.waiters.drain(..) {
for w in std::mem::take(&mut self.waiters) {

Copy link
Contributor Author

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?

@BewareMyPower
Copy link
Contributor Author

I tend to hold on this change until we do #102 so it has a good point to exclude the slab dep when unnecessary.

Agreed. Adding features could be good to separate some unnecessary dependencies.

Currently, slab does no harm compare with vec also, IMO.

From the perspective of performance, it's true. Comparing to Vec::push, the additional overhead of Slab::insert is only updating the next: usize field, which can be ignored in most cases.

So it's just a chore that makes code more reasonable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants