Skip to content

Fix missing enqueue notifications in timed_thread_context. - #2231

Open
daniel-schmidt wants to merge 1 commit into
NVIDIA:mainfrom
daniel-schmidt:timed_thread_scheduler_missing_wakeup
Open

Fix missing enqueue notifications in timed_thread_context.#2231
daniel-schmidt wants to merge 1 commit into
NVIDIA:mainfrom
daniel-schmidt:timed_thread_scheduler_missing_wakeup

Conversation

@daniel-schmidt

Copy link
Copy Markdown

The push_back of the command queue can return false during insertion of a new command.

See #2230 for details.

The push_back of the command queue can return false during insertion of a new command.
@copy-pr-bot

copy-pr-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

ready_ = true;
cv_.notify_one();
}
cv_.notify_one();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lewissbaker i am remembering a situation in which a cv notify needed to happen within a critical section. is this one of those situations?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm about 80% confident this is not required to be done inside the critical section.

The circumstance where you would need to notify inside the critical section is if the awoken work could cause the destruction of the condition variable before the notify method returns.

In this case, you're notifying the cv with a positive number of inflight submissions, which means, in the worst case, the run function is spinning on that count, waiting for it to go back to zero, which it can't do until notify_one returns. Since the only path to the cv's destruction is through the queue-draining loop in run, and since that loop must be no further advanced than spinning on the count of active submissions, I don't think you can run into UB here.

@lewissbaker lewissbaker Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one thing I'd want to check here is whether or not there is a possibility of the run() thread seeing the decrement of n_submissions_in_flight before it sees the effects of notify_one().

The decrement below has relaxed memory order and I don't think that the notify_one member-function provides acquire semantics that would prevent the subsequent access of n_submissions_in_flight_ from being reordered above the notify_one() call. So whether or not this is safe might depend on exactly how the notify_one() call is implemented - I'm not 100% sure on this one.

I would probably keep the call to notify_one() inside the scope of the lock just to be safe here.

EDIT: Actually, on reflection I think the better solution is to make the decrement below a release operation and then the compare-exchange in run() an acquire operation.

@ericniebler

Copy link
Copy Markdown
Collaborator

/ok to test b62e1c4

Comment on lines 227 to 229
n_submissions_in_flight_.compare_exchange_strong(n,
context_closed,
STDEXEC::__std::memory_order_relaxed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incidentally to this PR, I don't understand why this CAS is here.

If we're running this code then n < 0 must be true; that can only happen if run has exited the busy-loop that's trying to set n_submissions_in_flight_ to context_closed. Given that run has successfully exited the busy loop, there's nothing I can see that prevents the current context from being destroyed out from under you here. We're already playing with fire by having discovered that *this might be about to be destroyed by dereferencing this on line 214; continuing to dereference this after we know it's in its death throes seems like all downside and no upside.

Maybe there's a concern that, at shut-down, such a large number of attempts to schedule will happen "all at once" that context_closed gets incremented up to 0. I suppose that could theoretically happen and, if it did, it would be disastrous. That could be fixed by changing how "closed" is represented. I'd split n_submissions_in_flight_ into a one-bit "closed?" field in the LSB, and a count of in-flight submissions in the remaining bits; incrementing and decrementing the count could then be done with fetch_add(2, …) and fetch_sub(2, …), and the busy-loop that's trying to mark the context as closed could spinning around compare_exchange_weak(0, 1, …). The check for a negative n here would instead be a check for odd n. It's impossible to accidentally overflow the count field and corrupt the closed field with this strategy.

@ericniebler

Copy link
Copy Markdown
Collaborator

@daniel-schmidt could you please look into the clang-16 test failure?

STDEXEC_ASSERT(op->command_ == command_type::command_type::stop);
static_cast<stop_type*>(op)->set_value_(op);
}
n_submissions_in_flight_.compare_exchange_strong(n,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this line trying to change 'n' back to 'context_closed'?
The 'n' value obtained above was the value before 1 was added to it, so in the no-contention case this is going to fail (as the value is likely still n+1 rather than n).

This seems like it could potentially just set the value back to 'context_closed' or just decrement the value instead? Or alternatively, turn the above fetch_add into a compare_exchange loop that leaves the value unchanged if it's currently 'context_closed'.

Either way, some commentary here would be helpful for future readers.

STDEXEC::__spin_loop_pause();
expected = 0;
}
op = heap_.front();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop seems like it could spin for an unbounded amount of time if other threads keep queuing new items onto it as a single other thread could potentially continuously win the race of incrementing the counter to 1, then this loop checks the counter and spins, then the other thread decrements, then immediately enqueues another item (which increments the count to 1).

An alternative would be to atomically set a flag that means that new schedule operations are not permitted once the count reaches zero when the flag is set. This would put a bound on the number of other items that can be enqueued to the number of threads currently calling schedule() at the time that the shutdown begins.

Having said that, at a higher level I would probably try to avoid a situation where the users can still potentially schedule more items once the timed_thread_context destructor starts. I would instead make it a precondition of the destructor that all scheduled items have completed and that no further items will be scheduled onto the context. This would greatly simplify the shutdown approach - once you see the 'stop-requested' flag you just assert that the queue is empty and exit the run() function. You could also then completely get rid of the n_submissions_in_flight_ data-member.

ready_ = true;
cv_.notify_one();
}
cv_.notify_one();

@lewissbaker lewissbaker Aug 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one thing I'd want to check here is whether or not there is a possibility of the run() thread seeing the decrement of n_submissions_in_flight before it sees the effects of notify_one().

The decrement below has relaxed memory order and I don't think that the notify_one member-function provides acquire semantics that would prevent the subsequent access of n_submissions_in_flight_ from being reordered above the notify_one() call. So whether or not this is safe might depend on exactly how the notify_one() call is implemented - I'm not 100% sure on this one.

I would probably keep the call to notify_one() inside the scope of the lock just to be safe here.

EDIT: Actually, on reflection I think the better solution is to make the decrement below a release operation and then the compare-exchange in run() an acquire operation.

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.

4 participants