Skip to content

Commit 7ae6a59

Browse files
committed
pythongh-116738: Make _heapq module thread-safe
1 parent 3f9eb55 commit 7ae6a59

File tree

3 files changed

+133
-5
lines changed

3 files changed

+133
-5
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import unittest
2+
3+
import heapq
4+
5+
from threading import Thread, Barrier
6+
from random import shuffle, randint
7+
from unittest import TestCase
8+
9+
from test.support import threading_helper
10+
11+
12+
NTHREAD: int = 10
13+
OBJECT_COUNT: int = 5_000
14+
15+
16+
@threading_helper.requires_working_threading()
17+
class TestHeapq(TestCase):
18+
def test_racing_heapify(self):
19+
heap = list(range(OBJECT_COUNT))
20+
shuffle(heap)
21+
22+
barrier = Barrier(NTHREAD)
23+
24+
def heapify_func(heap: list[int]):
25+
barrier.wait()
26+
heapq.heapify(heap)
27+
28+
self.run_parallel(heapify_func, (heap,), NTHREAD)
29+
self.is_heap_property_satisfied(heap)
30+
31+
def test_racing_heappush(self):
32+
heap = []
33+
34+
barrier = Barrier(NTHREAD)
35+
36+
def heappush_func(heap):
37+
barrier.wait()
38+
for item in reversed(range(OBJECT_COUNT)):
39+
heapq.heappush(heap, item)
40+
41+
self.run_parallel(heappush_func, (heap,), NTHREAD)
42+
self.is_heap_property_satisfied(heap)
43+
44+
def test_racing_heappop(self):
45+
heap = list(range(OBJECT_COUNT))
46+
shuffle(heap)
47+
heapq.heapify(heap)
48+
49+
barrier = Barrier(NTHREAD)
50+
51+
# Each thread pops (OBJECT_COUNT / NTHREAD) items
52+
self.assertEqual(0, OBJECT_COUNT % NTHREAD)
53+
per_thread_pop_count = OBJECT_COUNT // NTHREAD
54+
55+
def heappop_func(heap, pop_count):
56+
barrier.wait()
57+
local_list = []
58+
for _ in range(pop_count):
59+
item = heapq.heappop(heap)
60+
local_list.append(item)
61+
62+
# Each local list should be sorted
63+
self.is_sorted(local_list)
64+
65+
self.run_parallel(heappop_func, (heap, per_thread_pop_count), NTHREAD)
66+
self.assertEqual(0, len(heap))
67+
68+
def test_racing_heappushpop(self):
69+
heap = list(range(OBJECT_COUNT))
70+
shuffle(heap)
71+
72+
heapq.heapify(heap)
73+
74+
barrier = Barrier(NTHREAD)
75+
76+
pushpop_items = [randint(-OBJECT_COUNT, OBJECT_COUNT) for _ in range(OBJECT_COUNT)]
77+
def heappushpop_func(heap, pushpop_items):
78+
barrier.wait()
79+
local_list = []
80+
for item in pushpop_items:
81+
popped_item = heapq.heappushpop(heap, item)
82+
self.assertTrue(popped_item <= item)
83+
84+
self.run_parallel(heappushpop_func, (heap, pushpop_items), NTHREAD)
85+
self.assertEqual(OBJECT_COUNT, len(heap))
86+
self.is_heap_property_satisfied(heap)
87+
88+
def is_heap_property_satisfied(self, heap: list[object]):
89+
# The value of a parent is always less than or equal to
90+
# the value of its children.
91+
# position 0 has no parent
92+
for pos in range(1, len(heap)):
93+
parent_pos = (pos - 1) >> 1
94+
if heap[parent_pos] > heap[pos]:
95+
return False
96+
97+
return True
98+
99+
def is_sorted(self, lst):
100+
return all(lst[i - 1] <= lst[i] for i in range(1, len(lst)))
101+
102+
@staticmethod
103+
def run_parallel(worker_func, args, nthreads):
104+
workers = []
105+
for _ in range(nthreads):
106+
worker = Thread(target=worker_func, args=args)
107+
workers.append(worker)
108+
worker.start()
109+
110+
for worker in workers:
111+
worker.join()
112+
113+
114+
if __name__ == "__main__":
115+
unittest.main()

Modules/_heapqmodule.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ siftup(PyListObject *heap, Py_ssize_t pos)
117117
}
118118

119119
/*[clinic input]
120+
@critical_section heap
120121
_heapq.heappush
121122
122123
heap: object(subclass_of='&PyList_Type')
@@ -128,7 +129,7 @@ Push item onto heap, maintaining the heap invariant.
128129

129130
static PyObject *
130131
_heapq_heappush_impl(PyObject *module, PyObject *heap, PyObject *item)
131-
/*[clinic end generated code: output=912c094f47663935 input=7c69611f3698aceb]*/
132+
/*[clinic end generated code: output=912c094f47663935 input=f7a4f03ef8d52e67]*/
132133
{
133134
if (PyList_Append(heap, item))
134135
return NULL;
@@ -171,6 +172,7 @@ heappop_internal(PyObject *heap, int siftup_func(PyListObject *, Py_ssize_t))
171172
}
172173

173174
/*[clinic input]
175+
@critical_section heap
174176
_heapq.heappop
175177
176178
heap: object(subclass_of='&PyList_Type')
@@ -181,7 +183,7 @@ Pop the smallest item off the heap, maintaining the heap invariant.
181183

182184
static PyObject *
183185
_heapq_heappop_impl(PyObject *module, PyObject *heap)
184-
/*[clinic end generated code: output=96dfe82d37d9af76 input=91487987a583c856]*/
186+
/*[clinic end generated code: output=96dfe82d37d9af76 input=ed396461b153dd51]*/
185187
{
186188
return heappop_internal(heap, siftup);
187189
}
@@ -232,6 +234,7 @@ _heapq_heapreplace_impl(PyObject *module, PyObject *heap, PyObject *item)
232234
}
233235

234236
/*[clinic input]
237+
@critical_section heap
235238
_heapq.heappushpop
236239
237240
heap: object(subclass_of='&PyList_Type')
@@ -246,7 +249,7 @@ a separate call to heappop().
246249

247250
static PyObject *
248251
_heapq_heappushpop_impl(PyObject *module, PyObject *heap, PyObject *item)
249-
/*[clinic end generated code: output=67231dc98ed5774f input=5dc701f1eb4a4aa7]*/
252+
/*[clinic end generated code: output=67231dc98ed5774f input=db05c81b1dd92c44]*/
250253
{
251254
PyObject *returnitem;
252255
int cmp;
@@ -371,6 +374,7 @@ heapify_internal(PyObject *heap, int siftup_func(PyListObject *, Py_ssize_t))
371374
}
372375

373376
/*[clinic input]
377+
@critical_section heap
374378
_heapq.heapify
375379
376380
heap: object(subclass_of='&PyList_Type')
@@ -381,7 +385,7 @@ Transform list into a heap, in-place, in O(len(heap)) time.
381385

382386
static PyObject *
383387
_heapq_heapify_impl(PyObject *module, PyObject *heap)
384-
/*[clinic end generated code: output=e63a636fcf83d6d0 input=53bb7a2166febb73]*/
388+
/*[clinic end generated code: output=e63a636fcf83d6d0 input=aaaaa028b9b6af08]*/
385389
{
386390
return heapify_internal(heap, siftup);
387391
}

Modules/clinic/_heapqmodule.c.h

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)