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}