Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Lib/test/test_free_threading/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,43 @@ def iterate():
)


def test_shared_iterator(self):
# Deque iterators are advanced under the deque's lock, so threads
# sharing one iterator must together see every item exactly once.
n = 20000
for make_iter in (iter, reversed):
with self.subTest(make_iter=make_iter.__name__):
d = deque(range(n))
it = make_iter(d)
results = []

def consume():
results.append(list(x for x in it))

threading_helper.run_concurrently([consume] * 4)
self.assertEqual(sorted(x for r in results for x in r),
list(range(n)))

def test_iterate_while_mutating(self):
d = deque(range(100))

def mutate():
for i in range(5000):
d.append(i)
d.popleft()

def iterate():
for _ in range(500):
it = iter(d)
try:
for _ in it:
pass
except RuntimeError:
pass
it.__length_hint__()
it.__reduce__()

threading_helper.run_concurrently([mutate, iterate, iterate])

if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve performance of iterating over a :class:`collections.deque` in the
:term:`free-threaded build` by locking only the deque, not the iterator.
29 changes: 19 additions & 10 deletions Modules/_collectionsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1966,7 +1966,7 @@ dequeiter_next_lock_held(dequeiterobject *it, dequeobject *deque)
PyObject *item;

if (it->deque->state != it->state) {
it->counter = 0;
FT_ATOMIC_STORE_SSIZE_RELAXED(it->counter, 0);
PyErr_SetString(PyExc_RuntimeError,
"deque mutated during iteration");
return NULL;
Expand All @@ -1978,7 +1978,7 @@ dequeiter_next_lock_held(dequeiterobject *it, dequeobject *deque)

item = it->b->data[it->index];
it->index++;
it->counter--;
FT_ATOMIC_STORE_SSIZE_RELAXED(it->counter, it->counter - 1);
if (it->index == BLOCKLEN && it->counter > 0) {
CHECK_NOT_END(it->b->rightlink);
it->b = it->b->rightlink;
Expand All @@ -1987,6 +1987,14 @@ dequeiter_next_lock_held(dequeiterobject *it, dequeobject *deque)
return Py_NewRef(item);
}

/* The mutable fields of a deque iterator (b, index and counter) are only
* written while holding the critical section of the deque it iterates over,
* never the iterator's own lock. Concurrent next() calls on a shared
* iterator are therefore serialized by the deque's lock, and locking the
* iterator as well is unnecessary. This halves the number of atomic
* operations per item in the free-threaded build. __length_hint__ reads
* counter without locking, so writes to it use relaxed atomic stores.
*/
static PyObject *
dequeiter_next(PyObject *op)
{
Expand All @@ -1995,9 +2003,9 @@ dequeiter_next(PyObject *op)
// It's safe to access it->deque without holding the per-object lock for it
// here; it->deque is only assigned during construction of it.
dequeobject *deque = it->deque;
Py_BEGIN_CRITICAL_SECTION2(it, deque);
Py_BEGIN_CRITICAL_SECTION(deque);
result = dequeiter_next_lock_held(it, deque);
Py_END_CRITICAL_SECTION2();
Py_END_CRITICAL_SECTION();

return result;
}
Expand Down Expand Up @@ -2056,10 +2064,10 @@ dequeiter_reduce(PyObject *op, PyObject *Py_UNUSED(dummy))
// here; it->deque is only assigned during construction of it.
dequeobject *deque = it->deque;
Py_ssize_t size, counter;
Py_BEGIN_CRITICAL_SECTION2(it, deque);
Py_BEGIN_CRITICAL_SECTION(deque);
size = Py_SIZE(deque);
counter = it->counter;
Py_END_CRITICAL_SECTION2();
Py_END_CRITICAL_SECTION();
return Py_BuildValue("O(On)", ty, deque, size - counter);
}

Expand Down Expand Up @@ -2119,7 +2127,7 @@ dequereviter_next_lock_held(dequeiterobject *it, dequeobject *deque)
return NULL;

if (it->deque->state != it->state) {
it->counter = 0;
FT_ATOMIC_STORE_SSIZE_RELAXED(it->counter, 0);
PyErr_SetString(PyExc_RuntimeError,
"deque mutated during iteration");
return NULL;
Expand All @@ -2129,7 +2137,7 @@ dequereviter_next_lock_held(dequeiterobject *it, dequeobject *deque)

item = it->b->data[it->index];
it->index--;
it->counter--;
FT_ATOMIC_STORE_SSIZE_RELAXED(it->counter, it->counter - 1);
if (it->index < 0 && it->counter > 0) {
CHECK_NOT_END(it->b->leftlink);
it->b = it->b->leftlink;
Expand All @@ -2146,9 +2154,10 @@ dequereviter_next(PyObject *self)
// It's safe to access it->deque without holding the per-object lock for it
// here; it->deque is only assigned during construction of it.
dequeobject *deque = it->deque;
Py_BEGIN_CRITICAL_SECTION2(it, deque);
// See the comment above dequeiter_next().
Py_BEGIN_CRITICAL_SECTION(deque);
item = dequereviter_next_lock_held(it, deque);
Py_END_CRITICAL_SECTION2();
Py_END_CRITICAL_SECTION();
return item;
}

Expand Down
Loading