Skip to content

Commit bcdbc93

Browse files
Fix PyDict issues on free-threaded build (#4788)
* clarify safety docs for PyDict API * get dict size using PyDict_Size on free-threaded build * avoid unnecessary critical sections in PyDict * add changelog entry * fix build error on GIL-enabled build * address code review comments * move attribute below doc comment * ignore unsafe_op_in_unsafe_fn in next_unchecked --------- Co-authored-by: David Hewitt <[email protected]>
1 parent 8aa6825 commit bcdbc93

File tree

2 files changed

+43
-18
lines changed

2 files changed

+43
-18
lines changed

newsfragments/4788.fixed.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
* Fixed thread-unsafe access of dict internals in BoundDictIterator on the
2+
free-threaded build.
3+
* Avoided creating unnecessary critical sections in BoundDictIterator
4+
implementation on the free-threaded build.

src/types/dict.rs

+39-18
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed {
181181
/// Iterates over the contents of this dictionary while holding a critical section on the dict.
182182
/// This is useful when the GIL is disabled and the dictionary is shared between threads.
183183
/// It is not guaranteed that the dictionary will not be modified during iteration when the
184-
/// closure calls arbitrary Python code that releases the current critical section.
184+
/// closure calls arbitrary Python code that releases the critical section held by the
185+
/// iterator. Otherwise, the dictionary will not be modified during iteration.
185186
///
186187
/// This method is a small performance optimization over `.iter().try_for_each()` when the
187188
/// nightly feature is not enabled because we cannot implement an optimised version of
@@ -396,19 +397,26 @@ impl<'a, 'py> Borrowed<'a, 'py, PyDict> {
396397
/// Iterates over the contents of this dictionary without incrementing reference counts.
397398
///
398399
/// # Safety
399-
/// It must be known that this dictionary will not be modified during iteration.
400+
/// It must be known that this dictionary will not be modified during iteration,
401+
/// for example, when parsing arguments in a keyword arguments dictionary.
400402
pub(crate) unsafe fn iter_borrowed(self) -> BorrowedDictIter<'a, 'py> {
401403
BorrowedDictIter::new(self)
402404
}
403405
}
404406

405407
fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t {
406-
#[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API))]
408+
#[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API, Py_GIL_DISABLED))]
407409
unsafe {
408410
ffi::PyDict_Size(dict.as_ptr())
409411
}
410412

411-
#[cfg(all(Py_3_8, not(PyPy), not(GraalPy), not(Py_LIMITED_API)))]
413+
#[cfg(all(
414+
Py_3_8,
415+
not(PyPy),
416+
not(GraalPy),
417+
not(Py_LIMITED_API),
418+
not(Py_GIL_DISABLED)
419+
))]
412420
unsafe {
413421
(*dict.as_ptr().cast::<ffi::PyDictObject>()).ma_used
414422
}
@@ -429,8 +437,11 @@ enum DictIterImpl {
429437
}
430438

431439
impl DictIterImpl {
440+
#[deny(unsafe_op_in_unsafe_fn)]
432441
#[inline]
433-
fn next<'py>(
442+
/// Safety: the dict should be locked with a critical section on the free-threaded build
443+
/// and otherwise not shared between threads in code that releases the GIL.
444+
unsafe fn next_unchecked<'py>(
434445
&mut self,
435446
dict: &Bound<'py, PyDict>,
436447
) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
@@ -440,7 +451,7 @@ impl DictIterImpl {
440451
remaining,
441452
ppos,
442453
..
443-
} => crate::sync::with_critical_section(dict, || {
454+
} => {
444455
let ma_used = dict_len(dict);
445456

446457
// These checks are similar to what CPython does.
@@ -470,20 +481,20 @@ impl DictIterImpl {
470481
let mut key: *mut ffi::PyObject = std::ptr::null_mut();
471482
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
472483

473-
if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) } != 0 {
484+
if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) != 0 } {
474485
*remaining -= 1;
475486
let py = dict.py();
476487
// Safety:
477488
// - PyDict_Next returns borrowed values
478489
// - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null
479490
Some((
480-
unsafe { key.assume_borrowed_unchecked(py) }.to_owned(),
481-
unsafe { value.assume_borrowed_unchecked(py) }.to_owned(),
491+
unsafe { key.assume_borrowed_unchecked(py).to_owned() },
492+
unsafe { value.assume_borrowed_unchecked(py).to_owned() },
482493
))
483494
} else {
484495
None
485496
}
486-
}),
497+
}
487498
}
488499
}
489500

@@ -504,7 +515,17 @@ impl<'py> Iterator for BoundDictIterator<'py> {
504515

505516
#[inline]
506517
fn next(&mut self) -> Option<Self::Item> {
507-
self.inner.next(&self.dict)
518+
#[cfg(Py_GIL_DISABLED)]
519+
{
520+
self.inner
521+
.with_critical_section(&self.dict, |inner| unsafe {
522+
inner.next_unchecked(&self.dict)
523+
})
524+
}
525+
#[cfg(not(Py_GIL_DISABLED))]
526+
{
527+
unsafe { self.inner.next_unchecked(&self.dict) }
528+
}
508529
}
509530

510531
#[inline]
@@ -522,7 +543,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
522543
{
523544
self.inner.with_critical_section(&self.dict, |inner| {
524545
let mut accum = init;
525-
while let Some(x) = inner.next(&self.dict) {
546+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
526547
accum = f(accum, x);
527548
}
528549
accum
@@ -539,7 +560,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
539560
{
540561
self.inner.with_critical_section(&self.dict, |inner| {
541562
let mut accum = init;
542-
while let Some(x) = inner.next(&self.dict) {
563+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
543564
accum = f(accum, x)?
544565
}
545566
R::from_output(accum)
@@ -554,7 +575,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
554575
F: FnMut(Self::Item) -> bool,
555576
{
556577
self.inner.with_critical_section(&self.dict, |inner| {
557-
while let Some(x) = inner.next(&self.dict) {
578+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
558579
if !f(x) {
559580
return false;
560581
}
@@ -571,7 +592,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
571592
F: FnMut(Self::Item) -> bool,
572593
{
573594
self.inner.with_critical_section(&self.dict, |inner| {
574-
while let Some(x) = inner.next(&self.dict) {
595+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
575596
if f(x) {
576597
return true;
577598
}
@@ -588,7 +609,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
588609
P: FnMut(&Self::Item) -> bool,
589610
{
590611
self.inner.with_critical_section(&self.dict, |inner| {
591-
while let Some(x) = inner.next(&self.dict) {
612+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
592613
if predicate(&x) {
593614
return Some(x);
594615
}
@@ -605,7 +626,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
605626
F: FnMut(Self::Item) -> Option<B>,
606627
{
607628
self.inner.with_critical_section(&self.dict, |inner| {
608-
while let Some(x) = inner.next(&self.dict) {
629+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
609630
if let found @ Some(_) = f(x) {
610631
return found;
611632
}
@@ -623,7 +644,7 @@ impl<'py> Iterator for BoundDictIterator<'py> {
623644
{
624645
self.inner.with_critical_section(&self.dict, |inner| {
625646
let mut acc = 0;
626-
while let Some(x) = inner.next(&self.dict) {
647+
while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } {
627648
if predicate(x) {
628649
return Some(acc);
629650
}

0 commit comments

Comments
 (0)