gh-155619: Fix data races in the context watcher registry - #155943
Open
cobed95 wants to merge 1 commit into
Open
gh-155619: Fix data races in the context watcher registry#155943cobed95 wants to merge 1 commit into
cobed95 wants to merge 1 commit into
Conversation
Author
Documentation build overview
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
#155619
Reproduction
gh-155619 was reported from source inspection without a reproducer, so here is one first. The new test registers a watcher whose callback does nothing, then races context switches against add/clear. On unpatched main it aborts every time:
Same binary, only the GIL toggled.
There are three races.
PyContext_AddWatcher()claimed a slot with a plain test-then-store, so two threads could take the same slot and get the same watcher ID. It then stored the callback and set the active bit as ordinary writes, so a notifier on a weakly ordered machine could see the bit before the pointer. Andnotify_context_watchers()reads the bitmask and then loads the slot, whichPyContext_ClearWatcher()can empty in between — that one is a plain interleaving, so no amount of memory ordering fixes it.Fix
The fix claims slots with
_Py_atomic_compare_exchange_ptr(), publishes the callback with a release store paired with an acquire load in notification, and skips a NULL callback instead of asserting on it, same as_PyDict_SendEvent(). No locks: notification runs on every context switch and does no atomic read-modify-write there. 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.rsthad no threading guidance at all, unlike dict and type watchers, so I added some.Testing
The new test passes 20/20 runs;
test_free_threading,test_context,test_capi,test_contextlibandtest_asyncioare clean on a free-threaded build (4735 tests), and the same binary withPYTHON_GIL=1is clean too.Benchmarks
pyperformance
async_tree,async_tree_io,async_generators(asyncio runs each task step throughctx.run()), plusnbodyas a control that never enters a context. Free-threaded build, baseline vs. this change:The default build was the same story, except
async_generatorscame 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-O3builds with--faston a laptop, so they answer "does this cost anything" and not much else; happy to redo them properly if it matters.No ThreadSanitizer run from me, though
test_free_threadingis inTSAN_TESTS, so CI should cover the new test.Approach discussed with @corona10 at the PyCon KR sprint.