Skip to content

Fix condition_variable abort_all/notify_one waking threads that already left the wait - #7879

Merged
hkaiser merged 1 commit into
masterfrom
fix-cv-abort-all-wakeup
Oct 26, 2026
Merged

hkaiser merged 1 commit into
masterfrom
fix-cv-abort-all-wakeup

Conversation

@hkaiser

@hkaiser hkaiser commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #7787, supersedes #7878.

Proposed Changes

  • abort_all() marks a waiter's queue entry as aborted while holding the lock and only resumes the thread (instead of forcing ctx.abort() after releasing the lock). The waiter turns the flag into an abort after re-acquiring the lock: wait() throws yield_aborted as before, wait_until() returns thread_restart_state::abort (already handled by timed_mutex::try_lock_until and condition_variable::wait_until).
  • While abort_all() drains the queue, new waiters are rejected (wait() throws yield_aborted, wait_until() returns abort). Since the lock is released for every woken thread, an aborted thread that waits again could otherwise keep refilling the queue and abort_all() would never finish.
  • Queue entries hold a counted reference (thread_id_ref_type) to the waiting thread. abort_all() and notify_one() wake the waiter through it instead of through its agent_ref, so a wakeup that arrives after the waiter has terminated is a no-op instead of throwing null_thread_id or waking an unrelated thread that reuses the thread_data.

Any background context you want to provide?

condition_variable_abort_stress (added in #7787) fails occasionally in CI with terminate called after throwing ... aborted (yield returned wait_abort): HPX(yield_aborted), e.g. in runs 40208092792, 40241656555, 40241996935, 40284047350 and 40307299100. The message format is the one from execution_agent::do_yield. abort_all() pops a waiter, releases the lock and only then calls ctx.abort(). By that time the waiter may already have left wait_until() on its own (100us timeout) and be re-acquiring the spinlock inside ~unlock_guard, spinning in yield_k with pending_boost. set_thread_state treats pending_boost as different from pending and stores the abort into the already queued thread, so do_yield throws yield_aborted from the (implicitly noexcept) destructor, which terminates the process. No change to the test can catch this, which is why #7878 does not help.

Widening the window between unlock and wakeup (a temporary 200us busy wait) also exposed the agent lifetime problem: in the final abort_all() after stop the waiter can exit and terminate before it is woken, and the late wakeup through its agent_ref threw null_thread_id in hpx_main. The same race exists in notify_one().

Testing (Windows, MSVC, Release and RelWithDebInfo): 2x100 runs of condition_variable_abort_stress with --hpx:queuing=local-workrequesting-fifo --hpx:threads=2 and with --hpx:threads=4, all synchronization and threading unit tests (50/50), and 300 runs with the forced 200us window, which crashed or hung before these changes. As a side effect a stress run takes about 0.15s instead of about 6s, since aborting threads that are still running no longer spins in set_thread_state.

condition_variable::abort_all() and notify_one() release the lock before
waking the waiter. By then the waiter may have left wait_until() on its
own (timeout) and even terminated:

- abort_all() used ctx.abort(), which lands on whatever suspension point
  the thread reaches next. When that is the spinlock backoff inside
  ~unlock_guard, yield_aborted is thrown from a noexcept destructor and
  the process terminates (the occasional condition_variable_abort_stress
  failures in CI).
- waking through the agent_ref of a thread that has terminated throws
  null_thread_id, or wakes an unrelated thread that reuses its
  thread_data.

abort_all() now marks the queue entry as aborted while holding the lock
and only resumes the thread; the waiter turns the flag into an abort
after re-acquiring the lock (wait() throws yield_aborted, wait_until()
returns thread_restart_state::abort). New waiters are rejected while
abort_all() drains the queue, which bounds the loop. Queue entries hold a
counted reference to the waiting thread, and waiters are woken through
it, so a late wakeup of a terminated thread is a no-op.

Signed-off-by: Hartmut Kaiser <[email protected]>
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 04a5cf1b-d625-4883-ba2d-0900fa309211
Commits

Reviewing files that changed from the base of the PR and between 55ef914 and dd0b260.

Files selected for processing: 2
  • libs/core/synchronization/include/hpx/synchronization/detail/condition_variable.hpp
  • libs/core/synchronization/src/detail/condition_variable.cpp

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of waiting operations when all waiters are aborted, so they receive an appropriate abort outcome instead of being forcefully stopped.
    • New waiters are rejected while an abort operation is draining the queue.
    • Improved notification and wake-up handling for waiters, including when an entry has no associated agent.

The condition variable now retains waiter thread IDs for wakeups. abort_all() drains and marks queued waiters, then wakes them. wait() and wait_until() report abort state when aborting begins or when a suspended waiter is marked.

Changes: Condition variable abort handling

Layer / File(s) Summary
Retain waiter identity and dispatch wakeups
libs/core/synchronization/src/detail/condition_variable.cpp
Queue entries retain counted thread IDs. notify_one() uses a shared wakeup helper to schedule HPX threads by ID or resume an agent reference.
Drain aborted waiters and report abort state
libs/core/synchronization/include/hpx/synchronization/detail/condition_variable.hpp, libs/core/synchronization/src/detail/condition_variable.cpp
A counter tracks abort drains. abort_all() marks and wakes removed waiters; wait() and wait_until() report abort state when the drain begins or marks a waiter.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Waiter
  participant condition_variable
  participant queue_entry
  participant resume_waiter
  participant HPX_scheduler
  Waiter->>condition_variable: call wait()
  condition_variable->>queue_entry: enqueue waiter
  condition_variable->>queue_entry: mark removed waiter aborted
  condition_variable->>resume_waiter: wake removed waiter
  resume_waiter->>HPX_scheduler: schedule retained thread ID
Loading

Possibly related PRs

  • TheHPXProject/hpx#7787: Fixes queue-pointer races in the same condition_variable implementation and adds an abort stress test.
  • TheHPXProject/hpx#7878: Updates the abort stress-test follow-up to handle yield_aborted and non-joinable workers.
Merge Risk: Minimal · up to dd0b2

No identified issue blocks merging the condition-variable wakeup and abort changes after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title names the primary change: condition_variable abort_all() and notify_one() no longer wake threads that already left the wait. The raw summary confirms the diff changes abort_all(), notify_one…
Description check Passed The description covers the changes in the diff. It explains that abort_all() marks queue entries as aborted under the lock, that new waiters are rejected while abort_all() drains the queue, and that q…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the primary change: condition_variable abort_all() and notify_one() no longer wake threads that already left the wait. The raw summary confirms the diff changes abort_all(), notify_one(), and wait paths in condition_variable.cpp, and adds the aborting_ member to the header. The title is specific and concise, and it matches the PR objectives.

Full details: Description check

Explanation

The description covers the changes in the diff. It explains that abort_all() marks queue entries as aborted under the lock, that new waiters are rejected while abort_all() drains the queue, and that queue entries hold a counted thread_id_ref_type that abort_all() and notify_one() use for wakeups. It also gives the failing CI symptom and the testing performed, so it is related to the changeset.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@hkaiser
hkaiser merged commit 2acdd1c into master Oct 26, 2026
84 checks passed
@hkaiser
hkaiser deleted the fix-cv-abort-all-wakeup branch October 26, 2026 21:09
@hkaiser hkaiser mentioned this pull request Oct 26, 2026
1 of 2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant