Skip to content

Commit 5a74e0e

Browse files
committed
gh-116738: Make grp module thread-safe
1 parent 8594d2c commit 5a74e0e

File tree

5 files changed

+132
-36
lines changed

5 files changed

+132
-36
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import unittest
2+
3+
from threading import Thread, Barrier
4+
from test.support import threading_helper
5+
6+
7+
def run_concurrently(worker_func, args, nthreads):
8+
"""
9+
Run the worker function concurrently in multiple threads.
10+
"""
11+
barrier = Barrier(nthreads)
12+
13+
def wrapper_func(*args):
14+
# Wait for all threads to reach this point before proceeding.
15+
barrier.wait()
16+
worker_func(*args)
17+
18+
with threading_helper.catch_threading_exception() as cm:
19+
workers = (
20+
Thread(target=wrapper_func, args=args) for _ in range(nthreads)
21+
)
22+
with threading_helper.start_threads(workers):
23+
pass
24+
25+
# If a worker thread raises an exception, re-raise it.
26+
if cm.exc_value is not None:
27+
raise cm.exc_value
28+
29+
30+
@threading_helper.requires_working_threading()
31+
class TestFTUtils(unittest.TestCase):
32+
def test_run_concurrently(self):
33+
lst = []
34+
35+
def worker(lst):
36+
lst.append(42)
37+
38+
nthreads = 10
39+
run_concurrently(worker, (lst,), nthreads)
40+
self.assertEqual(lst, [42] * nthreads)
41+
42+
def test_run_concurrently_raise(self):
43+
def worker():
44+
raise RuntimeError("Error")
45+
46+
nthreads = 3
47+
with self.assertRaises(RuntimeError):
48+
run_concurrently(worker, (), nthreads)
49+
50+
51+
if __name__ == "__main__":
52+
unittest.main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import unittest
2+
3+
from test.support import import_helper, threading_helper
4+
from test.test_free_threading.test_ft import run_concurrently
5+
6+
grp = import_helper.import_module("grp")
7+
8+
from test import test_grp
9+
10+
11+
NTHREADS = 10
12+
13+
14+
@threading_helper.requires_working_threading()
15+
class TestGrp(unittest.TestCase):
16+
def setUp(self):
17+
self.test_grp = test_grp.GroupDatabaseTestCase()
18+
19+
def test_racing_test_values(self):
20+
# test_grp.test_values() calls grp.getgrall() and checks the entries
21+
run_concurrently(
22+
worker_func=self.test_grp.test_values, args=(), nthreads=NTHREADS
23+
)
24+
25+
def test_racing_test_values_extended(self):
26+
# test_grp.test_values_extended() calls grp.getgrall(), grp.getgrgid(),
27+
# grp.getgrnam() and checks the entries
28+
run_concurrently(
29+
worker_func=self.test_grp.test_values_extended,
30+
args=(),
31+
nthreads=NTHREADS,
32+
)
33+
34+
35+
if __name__ == "__main__":
36+
unittest.main()

Lib/test/test_free_threading/test_heapq.py

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import heapq
44

55
from enum import Enum
6-
from threading import Thread, Barrier, Lock
6+
from threading import Barrier, Lock
77
from random import shuffle, randint
88

99
from test.support import threading_helper
10+
from test.test_free_threading.test_ft import run_concurrently
1011
from test import test_heapq
1112

1213

@@ -28,7 +29,7 @@ def test_racing_heapify(self):
2829
heap = list(range(OBJECT_COUNT))
2930
shuffle(heap)
3031

31-
self.run_concurrently(
32+
run_concurrently(
3233
worker_func=heapq.heapify, args=(heap,), nthreads=NTHREADS
3334
)
3435
self.test_heapq.check_invariant(heap)
@@ -40,7 +41,7 @@ def heappush_func(heap):
4041
for item in reversed(range(OBJECT_COUNT)):
4142
heapq.heappush(heap, item)
4243

43-
self.run_concurrently(
44+
run_concurrently(
4445
worker_func=heappush_func, args=(heap,), nthreads=NTHREADS
4546
)
4647
self.test_heapq.check_invariant(heap)
@@ -61,7 +62,7 @@ def heappop_func(heap, pop_count):
6162
# Each local list should be sorted
6263
self.assertTrue(self.is_sorted_ascending(local_list))
6364

64-
self.run_concurrently(
65+
run_concurrently(
6566
worker_func=heappop_func,
6667
args=(heap, per_thread_pop_count),
6768
nthreads=NTHREADS,
@@ -77,7 +78,7 @@ def heappushpop_func(heap, pushpop_items):
7778
popped_item = heapq.heappushpop(heap, item)
7879
self.assertTrue(popped_item <= item)
7980

80-
self.run_concurrently(
81+
run_concurrently(
8182
worker_func=heappushpop_func,
8283
args=(heap, pushpop_items),
8384
nthreads=NTHREADS,
@@ -93,7 +94,7 @@ def heapreplace_func(heap, replace_items):
9394
for item in replace_items:
9495
heapq.heapreplace(heap, item)
9596

96-
self.run_concurrently(
97+
run_concurrently(
9798
worker_func=heapreplace_func,
9899
args=(heap, replace_items),
99100
nthreads=NTHREADS,
@@ -105,7 +106,7 @@ def test_racing_heapify_max(self):
105106
max_heap = list(range(OBJECT_COUNT))
106107
shuffle(max_heap)
107108

108-
self.run_concurrently(
109+
run_concurrently(
109110
worker_func=heapq.heapify_max, args=(max_heap,), nthreads=NTHREADS
110111
)
111112
self.test_heapq.check_max_invariant(max_heap)
@@ -117,7 +118,7 @@ def heappush_max_func(max_heap):
117118
for item in range(OBJECT_COUNT):
118119
heapq.heappush_max(max_heap, item)
119120

120-
self.run_concurrently(
121+
run_concurrently(
121122
worker_func=heappush_max_func, args=(max_heap,), nthreads=NTHREADS
122123
)
123124
self.test_heapq.check_max_invariant(max_heap)
@@ -138,7 +139,7 @@ def heappop_max_func(max_heap, pop_count):
138139
# Each local list should be sorted
139140
self.assertTrue(self.is_sorted_descending(local_list))
140141

141-
self.run_concurrently(
142+
run_concurrently(
142143
worker_func=heappop_max_func,
143144
args=(max_heap, per_thread_pop_count),
144145
nthreads=NTHREADS,
@@ -154,7 +155,7 @@ def heappushpop_max_func(max_heap, pushpop_items):
154155
popped_item = heapq.heappushpop_max(max_heap, item)
155156
self.assertTrue(popped_item >= item)
156157

157-
self.run_concurrently(
158+
run_concurrently(
158159
worker_func=heappushpop_max_func,
159160
args=(max_heap, pushpop_items),
160161
nthreads=NTHREADS,
@@ -170,7 +171,7 @@ def heapreplace_max_func(max_heap, replace_items):
170171
for item in replace_items:
171172
heapq.heapreplace_max(max_heap, item)
172173

173-
self.run_concurrently(
174+
run_concurrently(
174175
worker_func=heapreplace_max_func,
175176
args=(max_heap, replace_items),
176177
nthreads=NTHREADS,
@@ -203,7 +204,7 @@ def worker():
203204
except IndexError:
204205
pass
205206

206-
self.run_concurrently(worker, (), n_threads * 2)
207+
run_concurrently(worker, (), n_threads * 2)
207208

208209
@staticmethod
209210
def is_sorted_ascending(lst):
@@ -241,27 +242,6 @@ def create_random_list(a, b, size):
241242
"""
242243
return [randint(-a, b) for _ in range(size)]
243244

244-
def run_concurrently(self, worker_func, args, nthreads):
245-
"""
246-
Run the worker function concurrently in multiple threads.
247-
"""
248-
barrier = Barrier(nthreads)
249-
250-
def wrapper_func(*args):
251-
# Wait for all threads to reach this point before proceeding.
252-
barrier.wait()
253-
worker_func(*args)
254-
255-
with threading_helper.catch_threading_exception() as cm:
256-
workers = (
257-
Thread(target=wrapper_func, args=args) for _ in range(nthreads)
258-
)
259-
with threading_helper.start_threads(workers):
260-
pass
261-
262-
# Worker threads should not raise any exceptions
263-
self.assertIsNone(cm.exc_value)
264-
265245

266246
if __name__ == "__main__":
267247
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make methods in :mod:`grp` thread-safe on the :term:`free threaded <free threading>` build.

Modules/grpmodule.c

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ grp_getgrgid_impl(PyObject *module, PyObject *id)
132132
if (!_Py_Gid_Converter(id, &gid)) {
133133
return NULL;
134134
}
135-
#ifdef HAVE_GETGRGID_R
135+
#if defined(HAVE_GETGRGID_R)
136136
int status;
137137
Py_ssize_t bufsize;
138138
/* Note: 'grp' will be used via pointer 'p' on getgrgid_r success. */
@@ -167,6 +167,17 @@ grp_getgrgid_impl(PyObject *module, PyObject *id)
167167
}
168168

169169
Py_END_ALLOW_THREADS
170+
#elif defined(Py_GIL_DISABLED)
171+
static PyMutex getgrgid_mutex = {0};
172+
PyMutex_Lock(&getgrgid_mutex);
173+
// The getgrgid() function need not be thread-safe.
174+
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/getgrgid.html
175+
p = getgrgid(gid);
176+
if (p == NULL) {
177+
// Unlock the mutex on error. The following error handling block will
178+
// handle the rest.
179+
PyMutex_Unlock(&getgrgid_mutex);
180+
}
170181
#else
171182
p = getgrgid(gid);
172183
#endif
@@ -183,8 +194,10 @@ grp_getgrgid_impl(PyObject *module, PyObject *id)
183194
return NULL;
184195
}
185196
retval = mkgrent(module, p);
186-
#ifdef HAVE_GETGRGID_R
197+
#if defined(HAVE_GETGRGID_R)
187198
PyMem_RawFree(buf);
199+
#elif defined(Py_GIL_DISABLED)
200+
PyMutex_Unlock(&getgrgid_mutex);
188201
#endif
189202
return retval;
190203
}
@@ -213,7 +226,7 @@ grp_getgrnam_impl(PyObject *module, PyObject *name)
213226
/* check for embedded null bytes */
214227
if (PyBytes_AsStringAndSize(bytes, &name_chars, NULL) == -1)
215228
goto out;
216-
#ifdef HAVE_GETGRNAM_R
229+
#if defined(HAVE_GETGRNAM_R)
217230
int status;
218231
Py_ssize_t bufsize;
219232
/* Note: 'grp' will be used via pointer 'p' on getgrnam_r success. */
@@ -248,6 +261,17 @@ grp_getgrnam_impl(PyObject *module, PyObject *name)
248261
}
249262

250263
Py_END_ALLOW_THREADS
264+
#elif defined(Py_GIL_DISABLED)
265+
static PyMutex getgrnam_mutex = {0};
266+
PyMutex_Lock(&getgrnam_mutex);
267+
// The getgrnam() function need not be thread-safe.
268+
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/getgrnam.html
269+
p = getgrnam(name_chars);
270+
if (p == NULL) {
271+
// Unlock the mutex on error. The following error handling block will
272+
// handle the rest.
273+
PyMutex_Unlock(&getgrnam_mutex);
274+
}
251275
#else
252276
p = getgrnam(name_chars);
253277
#endif
@@ -261,6 +285,9 @@ grp_getgrnam_impl(PyObject *module, PyObject *name)
261285
goto out;
262286
}
263287
retval = mkgrent(module, p);
288+
#if !defined(HAVE_GETGRNAM_R) && defined(Py_GIL_DISABLED)
289+
PyMutex_Unlock(&getgrnam_mutex);
290+
#endif
264291
out:
265292
PyMem_RawFree(buf);
266293
Py_DECREF(bytes);

0 commit comments

Comments
 (0)