Skip to content

Commit 0e11b6f

Browse files
committed
gh-158600: Preserve concurrent removals for several operands in set.intersection_update()
1 parent 19264ba commit 0e11b6f

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

‎Lib/test/test_free_threading/test_set.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,49 @@ def intersect(first, second):
210210

211211
self.assertEqual(target, expected)
212212

213+
def test_intersection_update_suspended_lock(self):
214+
"""Test an update while a later operand's lock is held."""
215+
NUM_ITERS = 200
216+
BLOCK_SIZE = self.SET_SIZE * 1_000
217+
HOLD_WORK = 10_000
218+
219+
initial = set(range(BLOCK_SIZE))
220+
blocked_operand = set(initial)
221+
evens = set(range(0, BLOCK_SIZE, 2))
222+
threes = set(range(0, BLOCK_SIZE, 3))
223+
expected = set(range(0, BLOCK_SIZE, 6))
224+
225+
class SlowEmpty:
226+
def __iter__(self):
227+
for _ in range(HOLD_WORK):
228+
pass
229+
return iter(())
230+
231+
# Repeat because the lock suspension depends on thread scheduling.
232+
for _ in range(NUM_ITERS):
233+
target = set(initial)
234+
235+
def hold_operand():
236+
blocked_operand.intersection(SlowEmpty())
237+
238+
def first_update():
239+
target.intersection_update(evens, blocked_operand)
240+
241+
def second_update():
242+
target.intersection_update(threes)
243+
244+
threads = [
245+
Thread(target=hold_operand),
246+
Thread(target=first_update),
247+
Thread(target=second_update),
248+
]
249+
for thread in threads:
250+
thread.start()
251+
for thread in threads:
252+
thread.join()
253+
254+
self.assertEqual(target, expected)
255+
213256
def test_iand_concurrent(self):
214257
"""Test concurrent &= operations on one shared set."""
215258
NUM_ITERS = 10

‎Objects/setobject.c‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1887,7 +1887,19 @@ set_intersection_update_multi_impl(PySetObject *so, PyObject * const *others,
18871887
Py_SETREF(result, newresult);
18881888
}
18891889
if (result != NULL) {
1890-
set_swap_bodies(so, (PySetObject *)result);
1890+
if (others_length == 1) {
1891+
set_swap_bodies(so, (PySetObject *)result);
1892+
}
1893+
else {
1894+
/* A later operand's critical section may have suspended
1895+
so's lock. Preserve concurrent removals by intersecting
1896+
the result with so's current contents. */
1897+
PyObject *updated = set_intersection_update(so, result);
1898+
if (updated == NULL) {
1899+
Py_CLEAR(result);
1900+
}
1901+
Py_XDECREF(updated);
1902+
}
18911903
}
18921904
}
18931905
Py_END_CRITICAL_SECTION2();

0 commit comments

Comments
 (0)