Skip to content

Commit 3198e2d

Browse files
committed
:pythongh-116738: Make _heapq module thread-safe
1 parent 3c05251 commit 3198e2d

File tree

3 files changed

+124
-4
lines changed

3 files changed

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

Modules/_heapqmodule.c

Lines changed: 6 additions & 3 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
}
@@ -371,6 +373,7 @@ heapify_internal(PyObject *heap, int siftup_func(PyListObject *, Py_ssize_t))
371373
}
372374

373375
/*[clinic input]
376+
@critical_section heap
374377
_heapq.heapify
375378
376379
heap: object(subclass_of='&PyList_Type')
@@ -381,7 +384,7 @@ Transform list into a heap, in-place, in O(len(heap)) time.
381384

382385
static PyObject *
383386
_heapq_heapify_impl(PyObject *module, PyObject *heap)
384-
/*[clinic end generated code: output=e63a636fcf83d6d0 input=53bb7a2166febb73]*/
387+
/*[clinic end generated code: output=e63a636fcf83d6d0 input=aaaaa028b9b6af08]*/
385388
{
386389
return heapify_internal(heap, siftup);
387390
}

Modules/clinic/_heapqmodule.c.h

Lines changed: 8 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)