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}