GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-155619: Fix data races in the context watcher registry by cobed95 · Pull Request #155943 · python/cpython

Conversation

@cobed95

@cobed95 cobed95 commented Aug 17, 2026 •
edited by bedevere-app Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

#155619

Reproduction

When threads race, the watcher registry hands out duplicate IDs and accepts duplicate clears. On current main the new tests fail on a free-threaded debug build:

Test Fails How
test_concurrent_add_clear_watchers 10/10 Two threads get the same ID, so one clear raises ValueError: No context watcher set for ID 0
test_concurrent_clear_same_watcher 10/10 Two, three or four clears of the same ID all succeed
test_clear_watcher_races_notification 9/10 The same duplicate-ID ValueError

The same binary with PYTHON_GIL=1 passes. (The double-clear test skips itself there, since the calls can't overlap.)

gh-159739 already replaced the assert(cb != NULL) in notify_context_watchers() with a NULL check, so a clear racing with a context switch no longer crashes. What's left is that the registry is read and written with plain accesses from several threads, and an add or clear isn't atomic as a whole.

Fix

PyContext_AddWatcher() and PyContext_ClearWatcher() now run under Py_BEGIN_CRITICAL_SECTION_MUTEX() on a new PyMutex in the interpreter state, the same pattern as TYPE_LOCK. They run about once per extension, so the lock costs nothing that matters.

notify_context_watchers() runs on every context switch and still takes no lock. It loads each callback with an acquire load, paired with the release store in PyContext_AddWatcher(), and keeps gh-159739's NULL check for a watcher cleared by an earlier callback or by another thread. I kept the bitmask rather than scanning all eight slots because it makes the common zero-watcher case one load and a not-taken branch.

Docs

Doc/c-api/contextvars.rst had no threading guidance at all, unlike dict and type watchers, so I added some.

Testing

The new tests pass 20/20 runs. test_free_threading, test_context, test_capi, test_contextlib and test_asyncio pass on a free-threaded debug build (5,149 tests), and so does the same binary with PYTHON_GIL=1 (1,690 tests).

test_concurrent_clear_same_watcher releases its threads with a busy-wait instead of a Barrier. A barrier wakes them too far apart to overlap and found nothing in 20,000 rounds, while the busy-wait hits the race in about a quarter of rounds.

No ThreadSanitizer run from me. It wouldn't catch the double clear anyway: in the previous revision every access was already atomic, so there was no data race to report, just clears that weren't atomic as a whole.

Benchmarks

pyperformance async_tree, async_tree_io, async_generators (asyncio runs each task step through ctx.run()), plus nbody as a control that never enters a context. Free-threaded build, baseline vs. this change:

Benchmark Baseline Patched
async_generators 245 ms +- 23 ms 236 ms +- 5 ms 1.04x faster, not significant
async_tree_io 374 ms +- 23 ms 363 ms +- 5 ms 1.03x faster, not significant
async_tree_none 175 ms +- 8 ms 177 ms +- 8 ms 1.01x slower, not significant
nbody (control) 77.0 ms +- 5.6 ms 76.6 ms +- 1.5 ms 1.01x faster, not significant

These were measured on the previous revision. Notification, the only path they exercise, hasn't changed since; add and clear now take a lock, but they aren't on that path.

The default build was the same story, except async_generators came out 1.08x slower there. I think that one is noise: its spread is about 12% of the mean, t=-2.54, and the same benchmark went faster on the free-threaded build. These were plain -O3 builds with --fast on a laptop, so they answer "does this cost anything" and not much else; happy to redo them properly if it matters.

  • Issue: PyContext watcher registry has data races in free-threaded builds #155619

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@cobed95
pythongh-155619: Fix data races in the context watcher registry
2b13f29
@cobed95
cobed95 requested a review from 1st1 as a code owner August 17, 2026 07:21
@bedevere-app bedevere-app Bot mentioned this pull request Aug 17, 2026
PyContext watcher registry has data races in free-threaded builds #155619
Open
@bedevere-app bedevere-app Bot added the awaiting review label Aug 17, 2026
@cobed95

cobed95 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Happily waiting for reviews from @hugovk and @corona10 :)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@corona10 corona10 added the sprint label Aug 17, 2026
@github-project-automation github-project-automation Bot added this to Sprint Aug 17, 2026
@github-project-automation github-project-automation Bot moved this to Todo in Sprint Aug 17, 2026
@corona10 corona10 self-assigned this Aug 17, 2026
@corona10 corona10 added the topic-free-threading label Aug 17, 2026
@read-the-docs-community

read-the-docs-community Bot commented Aug 17, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #35786850 | 📁 Comparing d297fc5 against main (c3df37c)

  🔍 Preview build  

450 files changed · + 10 added · ± 440 modified

+ Added

  • builtins/constants.html
  • builtins/exceptions.html
  • builtins/functions.html
  • builtins/index.html
  • builtins/stdtypes.html
  • builtins/threadsafety.html
  • builtins/time-complexity.html
  • c-api/pyexpat.html
  • library/time-complexity.html
  • library/tkinter.dialogs.html

± Modified

  • glossary.html
  • index.html
  • license.html
  • py-modindex.html
  • _static/tachyon-example-heatmap.html
  • c-api/arg.html
  • c-api/bool.html
  • c-api/buffer.html
  • c-api/bytearray.html
  • c-api/bytes.html
  • and 430 more...
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@florentinl florentinl mentioned this pull request Sep 2, 2026
Add safe context watcher API PyO3/pyo3#6227
Merged
@corona10

corona10 commented Sep 6, 2026

Copy link
Copy Markdown
Member

@cobed95 Sorry for the delay, I will take a look at this week.

cobed95 reacted with thumbs up emoji florentinl reacted with eyes emoji
All reactions
  • 👍 1 reaction
  • 👀 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@florentinl

florentinl commented Oct 20, 2026

Copy link
Copy Markdown

Thank you @cobed95 for looking into the issue I reported and putting together a fix!

@corona10, would you be able to review this when you have a chance? Thanks!

corona10 reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@corona10

corona10 commented Oct 20, 2026

Copy link
Copy Markdown
Member

Yeah I will do this during this sprint

florentinl reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

corona10
corona10 reviewed Oct 20, 2026
View reviewed changes
Comment thread Python/context.c Outdated
// lets two threads select the same slot and return the same ID.
// Losing the race means the slot is taken, so just try the next one.
PyContext_WatchCallback expected = NULL;
if (_Py_atomic_compare_exchange_ptr(&interp->context_watchers[i],

@corona10 corona10 Oct 20, 2026

Copy link
Copy Markdown
Member

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.

It doesn not gurentee to set set_context_watcher_bit if CAS is not success, you may run loop here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@cobed95 cobed95 Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

You’re right. The CAS only made claiming the slot atomic; setting the bit was still a separate step, so the add as a whole wasn’t. (It’s a strong CAS, so the claim itself didn’t need a retry loop.) I’ve dropped it: PyContext_AddWatcher() now does a plain check and store inside a critical section.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
corona10
corona10 reviewed Oct 20, 2026
View reviewed changes
Comment thread Python/context.c Outdated
Comment on lines +255 to +256
clear_context_watcher_bit(interp, watcher_id);
FT_ATOMIC_STORE_PTR_RELEASE(interp->context_watchers[watcher_id], NULL);

@corona10 corona10 Oct 20, 2026

Copy link
Copy Markdown
Member

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 will make issue if one thread clear watcher_id and other thread try to add watcher_id.
Seqeuntial operation should be gurenteed.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@cobed95 cobed95 Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

Agreed, and I could reproduce it. With the threads lined up, two concurrent PyContext_ClearWatcher() calls on the same ID both returned 0, and on current main sometimes three or four do. Add and Clear are now serialized by a critical section on a new per-interpreter mutex. Notification still doesn’t take it.

I added test_concurrent_clear_same_watcher for this. It fails every time on main and passes now.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
corona10
corona10 reviewed Oct 20, 2026
View reviewed changes
Comment thread Python/context.c Outdated
Comment on lines +251 to +254
// Stop notification selecting this slot before retiring the callback, so
// the window in which a notifier sees the bit but loads NULL is as short
// as possible. That window is benign: notify_context_watchers() skips a
// NULL callback.

@corona10 corona10 Oct 20, 2026

Copy link
Copy Markdown
Member

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.

Please rewrite comment as concise as possible.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@cobed95 cobed95 Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

Done, it’s one line now:

// notify_context_watchers() skips a NULL callback.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
corona10
corona10 requested changes Oct 20, 2026
View reviewed changes

@corona10 corona10 left a comment

Copy link
Copy Markdown
Member

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.

I thint that following functions should use critical section to gurentee sequetiallly consistant output.

  • PyContext_ClearWatcher
  • PyContext_AddWatcher

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Sprint Oct 20, 2026
@bedevere-app bedevere-app Bot added awaiting changes and removed awaiting review labels Oct 20, 2026
@bedevere-app

bedevere-app Bot commented Oct 20, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@cobed95

cobed95 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Hi everyone, I will work on this by the end of this weekend. Thanks for your patience.

florentinl reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

cobed95 added 2 commits November 10, 2026 23:18
@cobed95
Merge branch 'main' into pythongh-155619
54350da
Resolve the conflict with pythongh-159739 in notify_context_watchers(): keep the
acquire load, and cover both ways the callback can be NULL in the comment.
@cobed95
Serialize context watcher add and clear
56c8877
Each step of the lock-free version was atomic, but an add or clear as a
whole was not: two concurrent PyContext_ClearWatcher() calls on one ID
could both succeed, and a clear landing in the middle of an add could leave
the active bit set over an empty slot.

Run both under a critical section on a new per-interpreter mutex and drop
the CAS.  Notification still does not take the lock.

Add test_concurrent_clear_same_watcher, which fails on the previous
revision, and drop a test docstring's reference to the assert that
pythongh-159739 removed from main.  Update NEWS to match.
@cobed95

cobed95 commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Every access in the previous revision was atomic, so this was a race TSan couldn’t have flagged.

  • I've merged main. The only conflict was with Fix minor safety issues #159739 in notify_context_watchers(); I kept the acquire load along with its NULL check.
  • PyContext_AddWatcher() and PyContext_ClearWatcher() now run under Py_BEGIN_CRITICAL_SECTION_MUTEX() on a new PyMutex in the interpreter state, the same pattern as TYPE_LOCK. The CAS is gone.
  • notify_context_watchers() still doesn’t take the lock, so the earlier benchmark numbers still apply.
  • New test_concurrent_clear_same_watcher. It fails 10/10 on current main and skips itself when the GIL is enabled, since the calls can’t overlap then.
  • NEWS updated. Fix minor safety issues #159739 already stops the NULL call on main, so it now only describes the registry races.
  • The PR description has also been updated to account for the current state of the PR.

I have made the requested changes; please review again

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot added awaiting change review and removed awaiting changes labels Nov 10, 2026
@bedevere-app

bedevere-app Bot commented Nov 10, 2026

Copy link
Copy Markdown

Thanks for making the requested changes!

@corona10: please review the changes made to this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app
bedevere-app Bot requested a review from corona10 November 10, 2026 14:32
@cobed95
Don't abort the barrier when the double-clear test passes
d297fc5
The test aborted its barrier in a finally block, so on success it could
break the last round while the workers were still waking from it.  They
then raised BrokenBarrierError, which regrtest reports as an uncaught
thread exception, failing the free-threaded CI jobs under
--fail-env-changed.

Abort only when the main loop fails, and let the workers return if the
barrier is broken.
johng
johng reviewed Nov 10, 2026
View reviewed changes
Comment thread Lib/test/test_free_threading/test_context_watcher.py
Comment on lines +53 to +56
if sys._is_gil_enabled():
# The calls cannot overlap, and the busy-waits would just
# contend for the GIL.
self.skipTest("needs the GIL to be disabled")

@johng johng Nov 10, 2026

Copy link
Copy Markdown
Contributor

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.

Are all tests in this module already skip with the GIL enabled? There is a already https://github.com/python/cpython/blob/main/Lib/test/test_free_threading/__init__.py#L7

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Python/context.c
Comment on lines +204 to +208
#ifdef Py_GIL_DISABLED
(void)_Py_atomic_and_uint8(&interp->active_context_watchers, (uint8_t)~bit);
#else
interp->active_context_watchers &= (uint8_t)~bit;
#endif

@johng johng Nov 10, 2026

Copy link
Copy Markdown
Contributor

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.

should this be added as a new FT_ATOMIC_ wrapper?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread Lib/test/test_free_threading/test_context_watcher.py
Comment on lines +36 to +37
except RuntimeError:
continue # all CONTEXT_MAX_WATCHERS slots taken

@johng johng Nov 10, 2026

Copy link
Copy Markdown
Contributor

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.

Is there a reason to catch runtime errors which could mask test errors such as using too many slots?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

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

Reviewers

@1st1 1st1 Awaiting requested review from 1st1 1st1 is a code owner
@corona10 corona10 Awaiting requested review from corona10
1 more reviewer
@johng johng johng left review comments
Reviewers whose approvals may not affect merge requirements

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@corona10 corona10

Labels

awaiting change review sprint topic-free-threading

Projects

Sprint
Status: In Progress

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@cobed95 @corona10 @florentinl @johng
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.