pyo3/sync/
critical_section.rs

1//! Wrappers for the Python critical section API
2//!
3//! [Critical Sections](https://docs.python.org/3/c-api/init.html#python-critical-section-api) allow
4//! access to the [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex) lock attached to
5//! each Python object in the free-threaded build. They are no-ops on the GIL-enabled build.
6//!
7//! Provides weaker locking guarantees than traditional locks, but can in some cases be used to
8//! provide guarantees similar to the GIL without the risk of deadlocks associated with traditional
9//! locks.
10//!
11//! # Usage Notes
12//!
13//! The calling thread locks the per-object mutex when it enters the critical section and holds it
14//! until exiting the critical section unless the critical section is suspended. Any call into the
15//! CPython C API may cause the critical section to be suspended. Creating an inner critical
16//! section, for example by accessing an item in a Python list or dict, will cause the outer
17//! critical section to be relased while the inner critical section is active.
18//!
19//! As a consequence, it is only possible to lock one or two objects at a time. If you need two lock
20//! two objects, you should use the variants that accept two arguments. The outer critical section
21//! is suspended if you create an outer an inner critical section on two objects using the
22//! single-argument variants.
23//!
24//! It is not currently possible to lock more than two objects simultaneously using this mechanism.
25//! Taking a critical section on a container object does not lock the objects stored in the
26//! container.
27//!
28//! Many CPython C API functions do not lock the per-object mutex on objects passed to Python. You
29//! should not expect critical sections applied to built-in types to prevent concurrent
30//! modification. This API is most useful for user-defined types with full control over how the
31//! internal state for the type is managed.
32//!
33//! The caller must ensure the closure cannot implicitly release the critical section. If a
34//! multithreaded program calls back into the Python interpreter in a manner that would cause the
35//! critical section to be released, the per-object mutex will be unlocked and the state of the
36//! object may be read from or modified by another thread. Concurrent modifications are impossible,
37//! but races are possible and the state of an object may change "underneath" a suspended thread in
38//! possibly surprising ways.
39
40#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
41use crate::types::PyMutex;
42
43#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
44use crate::Python;
45use crate::{types::PyAny, Bound};
46#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
47use std::cell::UnsafeCell;
48
49#[cfg(Py_GIL_DISABLED)]
50struct CSGuard(crate::ffi::PyCriticalSection);
51
52#[cfg(Py_GIL_DISABLED)]
53impl Drop for CSGuard {
54    fn drop(&mut self) {
55        unsafe {
56            crate::ffi::PyCriticalSection_End(&mut self.0);
57        }
58    }
59}
60
61#[cfg(Py_GIL_DISABLED)]
62struct CS2Guard(crate::ffi::PyCriticalSection2);
63
64#[cfg(Py_GIL_DISABLED)]
65impl Drop for CS2Guard {
66    fn drop(&mut self) {
67        unsafe {
68            crate::ffi::PyCriticalSection2_End(&mut self.0);
69        }
70    }
71}
72
73/// Allows access to data protected by a PyMutex in a critical section
74///
75/// Used with the `with_critical_section_mutex` and
76/// `with_critical_section_mutex2` functions. See the documentation of those
77/// functions for more details.
78#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
79pub struct EnteredCriticalSection<'a, T>(&'a UnsafeCell<T>);
80
81#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
82impl<T> EnteredCriticalSection<'_, T> {
83    /// Get a mutable reference to the data wrapped by a PyMutex
84    ///
85    /// # Safety
86    ///
87    /// The caller must ensure the closure cannot implicitly release the critical section.
88    ///
89    /// If a multithreaded program calls back into the Python interpreter in a manner that would cause
90    /// the critical section to be released, the `PyMutex` will be unlocked and the resource protected
91    /// by the `PyMutex` may be read from or modified by another thread while the critical section is
92    /// suspended. Concurrent modifications are impossible, but races are possible and the state of the
93    /// protected resource may change in possibly surprising ways after calls into the interpreter.
94    pub unsafe fn get_mut(&mut self) -> &mut T {
95        unsafe { &mut *(self.0.get()) }
96    }
97
98    /// Get a immutable reference to the value wrapped by a PyMutex
99    ///
100    /// # Safety
101    ///
102    /// The caller must ensure the critical section is not released while the
103    /// reference is alive. If a multithreaded program calls back into the
104    /// Python interpreter in a manner that would cause the critical section to
105    /// be released, the `PyMutex` will be unlocked and the resource protected
106    /// by the `PyMutex` may be read from or modified by another thread while
107    /// the critical section is suspended and the thread that owns the reference
108    /// is blocked. Concurrent modifications are impossible, but races are
109    /// possible and the state of an object may change "underneath" a suspended
110    /// thread in possibly surprising ways. Note that many operations on Python
111    /// objects may call back into the interpreter in a blocking manner because
112    /// many C API calls can trigger the execution of arbitrary Python code.
113    pub unsafe fn get(&self) -> &T {
114        unsafe { &*(self.0.get()) }
115    }
116}
117
118/// Executes a closure with a Python critical section held on an object.
119///
120/// Locks the per-object mutex for the object `op` that is held while the closure `f` is
121/// executing. The critical section may be temporarily released and re-acquired if the closure calls
122/// back into the interpreter. See the notes in the
123/// [`pyo3::sync::critical_section`][crate::sync::critical_section] module documentation for more
124/// details.
125///
126/// This is structurally equivalent to the use of the paired Py_BEGIN_CRITICAL_SECTION and
127/// Py_END_CRITICAL_SECTION C-API macros.
128#[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))]
129pub fn with_critical_section<F, R>(object: &Bound<'_, PyAny>, f: F) -> R
130where
131    F: FnOnce() -> R,
132{
133    #[cfg(Py_GIL_DISABLED)]
134    {
135        let mut guard = CSGuard(unsafe { std::mem::zeroed() });
136        unsafe { crate::ffi::PyCriticalSection_Begin(&mut guard.0, object.as_ptr()) };
137        f()
138    }
139    #[cfg(not(Py_GIL_DISABLED))]
140    {
141        f()
142    }
143}
144
145/// Executes a closure with a Python critical section held on two objects.
146///
147/// Locks the per-object mutex for the objects `a` and `b` that are held while the closure `f` is
148/// executing. The critical section may be temporarily released and re-acquired if the closure calls
149/// back into the interpreter. See the notes in the
150/// [`pyo3::sync::critical_section`][crate::sync::critical_section] module documentation for more
151/// details.
152///
153/// This is structurally equivalent to the use of the paired
154/// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2 C-API macros.
155#[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))]
156pub fn with_critical_section2<F, R>(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, f: F) -> R
157where
158    F: FnOnce() -> R,
159{
160    #[cfg(Py_GIL_DISABLED)]
161    {
162        let mut guard = CS2Guard(unsafe { std::mem::zeroed() });
163        unsafe { crate::ffi::PyCriticalSection2_Begin(&mut guard.0, a.as_ptr(), b.as_ptr()) };
164        f()
165    }
166    #[cfg(not(Py_GIL_DISABLED))]
167    {
168        f()
169    }
170}
171
172/// Executes a closure with a Python critical section held on a `PyMutex`.
173///
174/// Locks the mutex `mutex` until the closure `f` finishes. The mutex may be temporarily unlocked
175/// and re-acquired if the closure calls back into the interpreter. See the notes in the
176/// [`pyo3::sync::critical_section`][crate::sync::critical_section] module documentation for more
177/// details.
178///
179/// This variant is particularly useful when paired with a global `PyMutex` to create a "local GIL"
180/// to protect global state in an extension in an analogous manner to the GIL without introducing
181/// any deadlock risks or affecting runtime behavior on the GIL-enabled build.
182///
183/// This is structurally equivalent to the use of the paired Py_BEGIN_CRITICAL_SECTION_MUTEX and
184/// Py_END_CRITICAL_SECTION C-API macros.
185///
186/// # Safety
187///
188/// The caller must ensure the closure cannot implicitly release the critical section. See the
189/// safety notes in the documentation for
190/// [`pyo3::sync::critical_section::EnteredCriticalSection`](crate::sync::critical_section::EnteredCriticalSection)
191/// for more details.
192#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
193#[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))]
194pub fn with_critical_section_mutex<F, R, T>(_py: Python<'_>, mutex: &PyMutex<T>, f: F) -> R
195where
196    F: for<'s> FnOnce(EnteredCriticalSection<'s, T>) -> R,
197{
198    #[cfg(Py_GIL_DISABLED)]
199    {
200        let mut guard = CSGuard(unsafe { std::mem::zeroed() });
201        unsafe { crate::ffi::PyCriticalSection_BeginMutex(&mut guard.0, &mut *mutex.mutex.get()) };
202        f(EnteredCriticalSection(&mutex.data))
203    }
204    #[cfg(not(Py_GIL_DISABLED))]
205    {
206        f(EnteredCriticalSection(&mutex.data))
207    }
208}
209
210/// Executes a closure with a Python critical section held on two `PyMutex` instances.
211///
212/// Simultaneously locks the mutexes `m1` and `m2` and holds them until the closure `f` is
213/// finished. The mutexes may be temporarily unlock and re-acquired if the closure calls back into
214/// the interpreter. See the notes in the
215/// [`pyo3::sync::critical_section`][crate::sync::critical_section] module documentation for more
216/// details.
217///
218/// This is structurally equivalent to the use of the paired
219/// Py_BEGIN_CRITICAL_SECTION2_MUTEX and Py_END_CRITICAL_SECTION2 C-API macros.
220///
221/// A no-op on GIL-enabled builds, where the critical section API is exposed as
222/// a no-op by the Python C API.
223///
224/// # Safety
225///
226/// The caller must ensure the closure cannot implicitly release the critical section. See the
227/// safety notes in the documentation for
228/// [`pyo3::sync::critical_section::EnteredCriticalSection`](crate::sync::critical_section::EnteredCriticalSection)
229/// for more details.
230#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
231#[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))]
232pub fn with_critical_section_mutex2<F, R, T1, T2>(
233    _py: Python<'_>,
234    m1: &PyMutex<T1>,
235    m2: &PyMutex<T2>,
236    f: F,
237) -> R
238where
239    F: for<'s> FnOnce(EnteredCriticalSection<'s, T1>, EnteredCriticalSection<'s, T2>) -> R,
240{
241    #[cfg(Py_GIL_DISABLED)]
242    {
243        let mut guard = CS2Guard(unsafe { std::mem::zeroed() });
244        unsafe {
245            crate::ffi::PyCriticalSection2_BeginMutex(
246                &mut guard.0,
247                &mut *m1.mutex.get(),
248                &mut *m2.mutex.get(),
249            )
250        };
251        f(
252            EnteredCriticalSection(&m1.data),
253            EnteredCriticalSection(&m2.data),
254        )
255    }
256    #[cfg(not(Py_GIL_DISABLED))]
257    {
258        f(
259            EnteredCriticalSection(&m1.data),
260            EnteredCriticalSection(&m2.data),
261        )
262    }
263}
264
265// We are building wasm Python with pthreads disabled and all these
266// tests use threads
267#[cfg(not(target_arch = "wasm32"))]
268#[cfg(test)]
269mod tests {
270    #[cfg(feature = "macros")]
271    use super::{with_critical_section, with_critical_section2};
272    #[cfg(all(not(Py_LIMITED_API), Py_3_14))]
273    use super::{with_critical_section_mutex, with_critical_section_mutex2};
274    #[cfg(all(not(Py_LIMITED_API), Py_3_14))]
275    use crate::types::PyMutex;
276    #[cfg(feature = "macros")]
277    use std::sync::atomic::{AtomicBool, Ordering};
278    #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))]
279    use std::sync::Barrier;
280
281    #[cfg(feature = "macros")]
282    use crate::Py;
283    #[cfg(any(feature = "macros", all(not(Py_LIMITED_API), Py_3_14)))]
284    use crate::Python;
285
286    #[cfg(feature = "macros")]
287    #[crate::pyclass(crate = "crate")]
288    struct VecWrapper(Vec<isize>);
289
290    #[cfg(feature = "macros")]
291    #[crate::pyclass(crate = "crate")]
292    struct BoolWrapper(AtomicBool);
293
294    #[cfg(feature = "macros")]
295    #[test]
296    fn test_critical_section() {
297        let barrier = Barrier::new(2);
298
299        let bool_wrapper = Python::attach(|py| -> Py<BoolWrapper> {
300            Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()
301        });
302
303        std::thread::scope(|s| {
304            s.spawn(|| {
305                Python::attach(|py| {
306                    let b = bool_wrapper.bind(py);
307                    with_critical_section(b, || {
308                        barrier.wait();
309                        std::thread::sleep(std::time::Duration::from_millis(10));
310                        b.borrow().0.store(true, Ordering::Release);
311                    })
312                });
313            });
314            s.spawn(|| {
315                barrier.wait();
316                Python::attach(|py| {
317                    let b = bool_wrapper.bind(py);
318                    // this blocks until the other thread's critical section finishes
319                    with_critical_section(b, || {
320                        assert!(b.borrow().0.load(Ordering::Acquire));
321                    });
322                });
323            });
324        });
325    }
326
327    #[cfg(all(not(Py_LIMITED_API), Py_3_14))]
328    #[test]
329    fn test_critical_section_mutex() {
330        let barrier = Barrier::new(2);
331
332        let mutex = PyMutex::new(false);
333
334        std::thread::scope(|s| {
335            s.spawn(|| {
336                Python::attach(|py| {
337                    with_critical_section_mutex(py, &mutex, |mut b| {
338                        barrier.wait();
339                        std::thread::sleep(std::time::Duration::from_millis(10));
340                        // SAFETY: we never call back into the python interpreter inside this critical section
341                        *(unsafe { b.get_mut() }) = true;
342                    });
343                });
344            });
345            s.spawn(|| {
346                barrier.wait();
347                Python::attach(|py| {
348                    // blocks until the other thread enters a critical section
349                    with_critical_section_mutex(py, &mutex, |b| {
350                        // SAFETY: we never call back into the python interpreter inside this critical section
351                        assert!(unsafe { *b.get() });
352                    });
353                });
354            });
355        });
356    }
357
358    #[cfg(feature = "macros")]
359    #[test]
360    fn test_critical_section2() {
361        let barrier = Barrier::new(3);
362
363        let (bool_wrapper1, bool_wrapper2) = Python::attach(|py| {
364            (
365                Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(),
366                Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(),
367            )
368        });
369
370        std::thread::scope(|s| {
371            s.spawn(|| {
372                Python::attach(|py| {
373                    let b1 = bool_wrapper1.bind(py);
374                    let b2 = bool_wrapper2.bind(py);
375                    with_critical_section2(b1, b2, || {
376                        barrier.wait();
377                        std::thread::sleep(std::time::Duration::from_millis(10));
378                        b1.borrow().0.store(true, Ordering::Release);
379                        b2.borrow().0.store(true, Ordering::Release);
380                    })
381                });
382            });
383            s.spawn(|| {
384                barrier.wait();
385                Python::attach(|py| {
386                    let b1 = bool_wrapper1.bind(py);
387                    // this blocks until the other thread's critical section finishes
388                    with_critical_section(b1, || {
389                        assert!(b1.borrow().0.load(Ordering::Acquire));
390                    });
391                });
392            });
393            s.spawn(|| {
394                barrier.wait();
395                Python::attach(|py| {
396                    let b2 = bool_wrapper2.bind(py);
397                    // this blocks until the other thread's critical section finishes
398                    with_critical_section(b2, || {
399                        assert!(b2.borrow().0.load(Ordering::Acquire));
400                    });
401                });
402            });
403        });
404    }
405
406    #[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
407    #[test]
408    fn test_critical_section_mutex2() {
409        let barrier = Barrier::new(2);
410
411        let m1 = PyMutex::new(false);
412        let m2 = PyMutex::new(false);
413
414        std::thread::scope(|s| {
415            s.spawn(|| {
416                Python::attach(|py| {
417                    with_critical_section_mutex2(py, &m1, &m2, |mut b1, mut b2| {
418                        barrier.wait();
419                        std::thread::sleep(std::time::Duration::from_millis(10));
420                        // SAFETY: we never call back into the python interpreter inside this critical section
421                        unsafe { (*b1.get_mut()) = true };
422                        unsafe { (*b2.get_mut()) = true };
423                    });
424                });
425            });
426            s.spawn(|| {
427                barrier.wait();
428                Python::attach(|py| {
429                    // blocks until the other thread enters a critical section
430                    with_critical_section_mutex2(py, &m1, &m2, |b1, b2| {
431                        // SAFETY: we never call back into the python interpreter inside this critical section
432                        assert!(unsafe { *b1.get() });
433                        assert!(unsafe { *b2.get() });
434                    });
435                });
436            });
437        });
438    }
439
440    #[cfg(feature = "macros")]
441    #[test]
442    fn test_critical_section2_same_object_no_deadlock() {
443        let barrier = Barrier::new(2);
444
445        let bool_wrapper = Python::attach(|py| -> Py<BoolWrapper> {
446            Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()
447        });
448
449        std::thread::scope(|s| {
450            s.spawn(|| {
451                Python::attach(|py| {
452                    let b = bool_wrapper.bind(py);
453                    with_critical_section2(b, b, || {
454                        barrier.wait();
455                        std::thread::sleep(std::time::Duration::from_millis(10));
456                        b.borrow().0.store(true, Ordering::Release);
457                    })
458                });
459            });
460            s.spawn(|| {
461                barrier.wait();
462                Python::attach(|py| {
463                    let b = bool_wrapper.bind(py);
464                    // this blocks until the other thread's critical section finishes
465                    with_critical_section(b, || {
466                        assert!(b.borrow().0.load(Ordering::Acquire));
467                    });
468                });
469            });
470        });
471    }
472
473    #[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
474    #[test]
475    fn test_critical_section_mutex2_same_object_no_deadlock() {
476        let barrier = Barrier::new(2);
477
478        let m = PyMutex::new(false);
479
480        std::thread::scope(|s| {
481            s.spawn(|| {
482                Python::attach(|py| {
483                    with_critical_section_mutex2(py, &m, &m, |mut b1, b2| {
484                        barrier.wait();
485                        std::thread::sleep(std::time::Duration::from_millis(10));
486                        // SAFETY: we never call back into the python interpreter inside this critical section
487                        unsafe { (*b1.get_mut()) = true };
488                        assert!(unsafe { *b2.get() });
489                    });
490                });
491            });
492            s.spawn(|| {
493                barrier.wait();
494                Python::attach(|py| {
495                    // this blocks until the other thread's critical section finishes
496                    with_critical_section_mutex(py, &m, |b| {
497                        // SAFETY: we never call back into the python interpreter inside this critical section
498                        assert!(unsafe { *b.get() });
499                    });
500                });
501            });
502        });
503    }
504
505    #[cfg(feature = "macros")]
506    #[test]
507    fn test_critical_section2_two_containers() {
508        let (vec1, vec2) = Python::attach(|py| {
509            (
510                Py::new(py, VecWrapper(vec![1, 2, 3])).unwrap(),
511                Py::new(py, VecWrapper(vec![4, 5])).unwrap(),
512            )
513        });
514
515        std::thread::scope(|s| {
516            s.spawn(|| {
517                Python::attach(|py| {
518                    let v1 = vec1.bind(py);
519                    let v2 = vec2.bind(py);
520                    with_critical_section2(v1, v2, || {
521                        // v2.extend(v1)
522                        v2.borrow_mut().0.extend(v1.borrow().0.iter());
523                    })
524                });
525            });
526            s.spawn(|| {
527                Python::attach(|py| {
528                    let v1 = vec1.bind(py);
529                    let v2 = vec2.bind(py);
530                    with_critical_section2(v1, v2, || {
531                        // v1.extend(v2)
532                        v1.borrow_mut().0.extend(v2.borrow().0.iter());
533                    })
534                });
535            });
536        });
537
538        Python::attach(|py| {
539            let v1 = vec1.bind(py);
540            let v2 = vec2.bind(py);
541            // execution order is not guaranteed, so we need to check both
542            // NB: extend should be atomic, items must not be interleaved
543            // v1.extend(v2)
544            // v2.extend(v1)
545            let expected1_vec1 = vec![1, 2, 3, 4, 5];
546            let expected1_vec2 = vec![4, 5, 1, 2, 3, 4, 5];
547            // v2.extend(v1)
548            // v1.extend(v2)
549            let expected2_vec1 = vec![1, 2, 3, 4, 5, 1, 2, 3];
550            let expected2_vec2 = vec![4, 5, 1, 2, 3];
551
552            assert!(
553                (v1.borrow().0.eq(&expected1_vec1) && v2.borrow().0.eq(&expected1_vec2))
554                    || (v1.borrow().0.eq(&expected2_vec1) && v2.borrow().0.eq(&expected2_vec2))
555            );
556        });
557    }
558
559    #[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
560    #[test]
561    fn test_critical_section_mutex2_two_containers() {
562        let (m1, m2) = (PyMutex::new(vec![1, 2, 3]), PyMutex::new(vec![4, 5]));
563
564        let (m1_guard, m2_guard) = (m1.lock().unwrap(), m2.lock().unwrap());
565
566        std::thread::scope(|s| {
567            s.spawn(|| {
568                Python::attach(|py| {
569                    with_critical_section_mutex2(py, &m1, &m2, |mut v1, v2| {
570                        // v1.extend(v1)
571                        // SAFETY: we never call back into the python interpreter inside this critical section
572                        let vec1 = unsafe { v1.get_mut() };
573                        let vec2 = unsafe { v2.get() };
574                        vec1.extend(vec2.iter());
575                    })
576                });
577            });
578            s.spawn(|| {
579                Python::attach(|py| {
580                    with_critical_section_mutex2(py, &m1, &m2, |v1, mut v2| {
581                        // v2.extend(v1)
582                        // SAFETY: we never call back into the python interpreter inside this critical section
583                        let vec1 = unsafe { v1.get() };
584                        let vec2 = unsafe { v2.get_mut() };
585                        vec2.extend(vec1.iter());
586                    })
587                });
588            });
589            // the other threads waiting for locks should not block this attach
590            Python::attach(|_| {
591                // On the free-threaded build, the critical sections should have blocked
592                // the other threads from modification.
593                #[cfg(Py_GIL_DISABLED)]
594                {
595                    assert_eq!(&*m1_guard, &[1, 2, 3]);
596                    assert_eq!(&*m2_guard, &[4, 5]);
597                }
598            });
599            drop(m1_guard);
600            drop(m2_guard);
601        });
602
603        // execution order is not guaranteed, so we need to check both
604        // NB: extend should be atomic, items must not be interleaved
605        // v1.extend(v2)
606        // v2.extend(v1)
607        let expected1_vec1 = vec![1, 2, 3, 4, 5];
608        let expected1_vec2 = vec![4, 5, 1, 2, 3, 4, 5];
609        // v2.extend(v1)
610        // v1.extend(v2)
611        let expected2_vec1 = vec![1, 2, 3, 4, 5, 1, 2, 3];
612        let expected2_vec2 = vec![4, 5, 1, 2, 3];
613
614        let v1 = m1.lock().unwrap();
615        let v2 = m2.lock().unwrap();
616        assert!(
617            (&*v1, &*v2) == (&expected1_vec1, &expected1_vec2)
618                || (&*v1, &*v2) == (&expected2_vec1, &expected2_vec2)
619        );
620    }
621}