Skip to content

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
/// while the closure `f` is executing. The critical section may be temporarily
/// released and re-acquired if the closure calls back into the interpreter in
/// a manner that would block. This is similar to how the GIL can be released
/// during blocking calls. See the safety notes below for caveats about

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
/// until the closure `f` is finished.
/// while the closure `f` is executing. The critical section may be temporarily
/// released and re-acquired if the closure calls back into the interpreter in
/// a manner that would block. This is similar to how the GIL can be released
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
/// until the closure `f` is finished.
/// while the closure `f` is executing. The critical section may be temporarily
/// released and re-acquired if the closure calls back into the interpreter in
/// a manner that would block. This is similar to how the GIL can be released
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
///
/// Only available on Python 3.14 and newer.
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
/// The caller must ensure the closure cannot implicitly release the critical
/// section. If a multithreaded program calls back into the Python interpreter
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
#[test]
fn test_critical_section_mutex2_two_containers() {
let (m1, m2) = (PyMutex::new(vec![1, 2, 3]), PyMutex::new(vec![4, 5]));

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
((*v1).eq(&expected1_vec1) && (*v2).eq(&expected1_vec2))
|| ((*v1).eq(&expected2_vec1) && (*v2).eq(&expected2_vec2))
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
f: F,
) -> R
where
F: FnOnce(EnteredCriticalSection<'a, T>) -> R,
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
f: F,
) -> R
where
F: FnOnce(EnteredCriticalSection<'a, T1>, EnteredCriticalSection<'a, T2>) -> R,
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.

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