Skip to content

Expose unsafe wrappers for Py_BEGIN_CRITICAL_SECTION_MUTEX API#5642

Merged
ngoldbaum merged 32 commits intoPyO3:mainfrom
ngoldbaum:critical-section-mutex-2
Dec 12, 2025
Merged

Expose unsafe wrappers for Py_BEGIN_CRITICAL_SECTION_MUTEX API#5642
ngoldbaum merged 32 commits intoPyO3:mainfrom
ngoldbaum:critical-section-mutex-2

Conversation

@ngoldbaum
Copy link
Contributor

@ngoldbaum ngoldbaum commented Nov 21, 2025

Python 3.14 introduced new variants for the critical section macros that accept a PyMutex instead of a PyObject. See python/cpython#133296, capi-workgroup/decisions#67, and python/cpython#135899 for more details.

I also was responsible for making sure this landed in 3.14, so shipping this PR is special to me as it completes the loop :)

So far I don't think there are any open source uses yet because it's so new and it's 3.14-specific.

The new APIs accept an EnteredCriticalSection which exposes unsafe get and get_mut methods because the caller needs to guarantee that the closure doesn't unsafely release the resource protected by the PyMutex.

I also expanded the docs for the existing critical section API wrappers to clarify things a bit.

@ngoldbaum ngoldbaum marked this pull request as draft November 21, 2025 20:55
@ngoldbaum

This comment was marked as resolved.

@ngoldbaum ngoldbaum force-pushed the critical-section-mutex-2 branch from 5830242 to 0621775 Compare November 24, 2025 19:50
src/sync.rs Outdated

This comment was marked as outdated.

@ngoldbaum ngoldbaum marked this pull request as ready for review November 24, 2025 20:43
@ngoldbaum
Copy link
Contributor Author

I marked this ready for review but I expect it to have at least one more round. I left a couple questions for others and comments for myself inline.

src/sync.rs Outdated
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// a manner that would block. This is similar to how the GIL can be released
/// a manner that would block. This is similar to how the GIL can be released

src/sync.rs Outdated
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// a manner that would block. This is similar to how the GIL can be released
/// a manner that would block. This is similar to how the GIL can be released

src/sync.rs Outdated
Comment on lines 730 to 731
Copy link
Member

Choose a reason for hiding this comment

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

I would move this above the Safety section.

src/sync.rs Outdated
Comment on lines 720 to 721
Copy link
Member

Choose a reason for hiding this comment

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

We should probably state somewhere here that critical sections cannot be nested, and point from with_critical_section to with_critical_section2 if two locks / objects need to be locked simultaneously.

To reduce redundancy, I wonder if we should have a new module pyo3::sync::critical_sections, move all these APIs in there and have module-level documentation with a lot of this prose?

src/sync.rs Outdated
Copy link
Member

Choose a reason for hiding this comment

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

I wonder, would the test be more interesting if we lock m1 and / or m2 here, and then below the second s.spawn call we add something like

// the other threads waiting for locks should not block this attach
Python::attach(|py| {
    // on free-threaded build, the critical sections should have blocked
    // the other threads from modification.
    #[cfg(Py_GIL_DISABLED)] 
    {
        assert_eq!(&*m1_guard, &[1, 2, 3]);
        assert_eq!(&*m2_guard, &[4, 5]);
    }

    drop(m1_guard);
    drop(m2_guard);
});

... so that way, the two threads both will wait until the locks are released? We should still be deadlock free, I believe.

Suggested change
let m1_guard = m1.lock().unwrap();
let m2_guard = m2.lock().unwrap();

src/sync.rs Outdated
Comment on lines 1648 to 1649
Copy link
Member

Choose a reason for hiding this comment

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

Might read a bit easier to do the comparisons against tuples:

Suggested change
((*v1).eq(&expected1_vec1) && (*v2).eq(&expected1_vec2))
|| ((*v1).eq(&expected2_vec1) && (*v2).eq(&expected2_vec2))
(&*v1, &*v2) == (&expected1_vec1, &expected1_vec2))
|| (&*v1, &*v2) == (&expected2_vec1, &expected2_vec2))

src/sync.rs Outdated
Copy link
Member

Choose a reason for hiding this comment

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

I think this needs to be disconnected from the lifetime 'a

Suggested change
F: FnOnce(EnteredCriticalSection<'a, T>) -> R,
F: for<'section> FnOnce(EnteredCriticalSection<'section, T>) -> R,

... my reason is that otherwise it's possible to build code which "leaks" the section guard into the enclosing scope, as long as the borrow 'a on the mutex is longer:

let m = &mutex;
let mut outer_b = None;
with_critical_section_mutex(py, &mutex, |mut b| {
    // this assignment should be illegal
    outer_b = Some(b);
});
unsafe { outer_b.unwrap().get_mut() };

The good news is that you can then make the 'py and 'a lifetimes anonymous.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice! TIL about higher-ranked trait bounds.

src/sync.rs Outdated
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the above

Suggested change
F: FnOnce(EnteredCriticalSection<'a, T1>, EnteredCriticalSection<'a, T2>) -> R,
F: for<'section> FnOnce(EnteredCriticalSection<'section, T1>, EnteredCriticalSection<'section, T2>) -> R,

@ngoldbaum ngoldbaum force-pushed the critical-section-mutex-2 branch from 6842604 to d2fb744 Compare December 4, 2025 00:02
@ngoldbaum
Copy link
Contributor Author

I think the last push satisfies all your comments and concerns.

@ngoldbaum
Copy link
Contributor Author

I think tests are green now. I also added a release note for the moved functions.

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

Looks great, just some very tiny nits. Thanks for this 🚀

@ngoldbaum ngoldbaum force-pushed the critical-section-mutex-2 branch from f82aa98 to 2b6946a Compare December 12, 2025 19:57
@ngoldbaum ngoldbaum added this pull request to the merge queue Dec 12, 2025
Merged via the queue into PyO3:main with commit f46c9c4 Dec 12, 2025
43 checks passed
@ngoldbaum ngoldbaum deleted the critical-section-mutex-2 branch December 12, 2025 21:16
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.

2 participants

Comments