Skip to main content

pyo3/sync/
critical_section.rs

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