From 5e4d174a460ad3d9a1713a5cc7901f6bbbbbcd3d Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 8 Jul 2025 19:50:41 -0700 Subject: [PATCH 1/5] GH-116738: document thread-safety of bisect --- Doc/library/bisect.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Doc/library/bisect.rst b/Doc/library/bisect.rst index 78da563397b625..72e6d34fa82dc3 100644 --- a/Doc/library/bisect.rst +++ b/Doc/library/bisect.rst @@ -24,6 +24,16 @@ method to determine whether a value has been found. Instead, the functions only call the :meth:`~object.__lt__` method and will return an insertion point between values in an array. + .. note:: + + The functions in this module are not thread-safe. If multiple threads + concurrently use :mod:`bisect` functions on the same sequence, this + may result in undefined behaviour. Likewise, if the provided sequence + is mutated by a different thread while a :mod:`bisect` function + is operating on it, the result is undefined. For example, using + :py:func:`~bisect.insort_left` on the same list from multiple threads + may result in the list becoming unsorted. + .. _bisect functions: The following functions are provided: From 4b7f113043babbd1fc5417986888a57823523750 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 11 Jul 2025 11:04:25 -0700 Subject: [PATCH 2/5] Add free-threaded test for bisect. --- Lib/test/test_free_threading/test_bisect.py | 79 +++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Lib/test/test_free_threading/test_bisect.py diff --git a/Lib/test/test_free_threading/test_bisect.py b/Lib/test/test_free_threading/test_bisect.py new file mode 100644 index 00000000000000..d1a8eab6fb10a9 --- /dev/null +++ b/Lib/test/test_free_threading/test_bisect.py @@ -0,0 +1,79 @@ +import unittest +from test.support import import_helper, threading_helper +from threading import Thread, Barrier +import random + +py_bisect = import_helper.import_fresh_module('bisect', blocked=['_bisect']) +c_bisect = import_helper.import_fresh_module('bisect', fresh=['_bisect']) + + +NTHREADS = 4 +OBJECT_COUNT = 500 + + +class TestBase: + def do_racing_insort(self, insert_method): + def insert(data): + for _ in range(OBJECT_COUNT): + x = random.randint(-OBJECT_COUNT, OBJECT_COUNT) + insert_method(data, x) + + data = list(range(OBJECT_COUNT)) + self.run_concurrently( + worker_func=insert, args=(data,), nthreads=NTHREADS + ) + if False: + # These functions are not thread-safe and so the list can become + # unsorted. However, we don't want Python to crash if these + # functions are used concurrently on the same sequence. This + # should also not produce any TSAN warnings. + self.assertTrue(self.is_sorted_ascending(data)) + + def test_racing_insert_right(self): + self.do_racing_insort(self.mod.insort_right) + + def test_racing_insert_left(self): + self.do_racing_insort(self.mod.insort_left) + + @staticmethod + def is_sorted_ascending(lst): + """ + Check if the list is sorted in ascending order (non-decreasing). + """ + return all(lst[i - 1] <= lst[i] for i in range(1, len(lst))) + + def run_concurrently(self, worker_func, args, nthreads): + """ + Run the worker function concurrently in multiple threads. + """ + barrier = Barrier(nthreads) + + def wrapper_func(*args): + # Wait for all threads to reach this point before proceeding. + barrier.wait() + worker_func(*args) + + with threading_helper.catch_threading_exception() as cm: + workers = ( + Thread(target=wrapper_func, args=args) + for _ in range(nthreads) + ) + with threading_helper.start_threads(workers): + pass + + # Worker threads should not raise any exceptions + self.assertIsNone(cm.exc_value) + + +@threading_helper.requires_working_threading() +class TestPyBisect(unittest.TestCase, TestBase): + mod = py_bisect + + +@threading_helper.requires_working_threading() +class TestCBisect(unittest.TestCase, TestBase): + mod = c_bisect + + +if __name__ == "__main__": + unittest.main() From e405cf82ab16fccc2651ceb203b598d0b76904fd Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 14 Jul 2025 11:41:50 -0700 Subject: [PATCH 3/5] Adjust indent of "note" block. --- Doc/library/bisect.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Doc/library/bisect.rst b/Doc/library/bisect.rst index 72e6d34fa82dc3..d02ffe469adb1a 100644 --- a/Doc/library/bisect.rst +++ b/Doc/library/bisect.rst @@ -24,15 +24,15 @@ method to determine whether a value has been found. Instead, the functions only call the :meth:`~object.__lt__` method and will return an insertion point between values in an array. - .. note:: - - The functions in this module are not thread-safe. If multiple threads - concurrently use :mod:`bisect` functions on the same sequence, this - may result in undefined behaviour. Likewise, if the provided sequence - is mutated by a different thread while a :mod:`bisect` function - is operating on it, the result is undefined. For example, using - :py:func:`~bisect.insort_left` on the same list from multiple threads - may result in the list becoming unsorted. +.. note:: + + The functions in this module are not thread-safe. If multiple threads + concurrently use :mod:`bisect` functions on the same sequence, this + may result in undefined behaviour. Likewise, if the provided sequence + is mutated by a different thread while a :mod:`bisect` function + is operating on it, the result is undefined. For example, using + :py:func:`~bisect.insort_left` on the same list from multiple threads + may result in the list becoming unsorted. .. _bisect functions: From 87a90f3c26d4be3ec4e22ef5884b911b3a015de1 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 23 Jul 2025 09:13:24 -0700 Subject: [PATCH 4/5] Use `threading_helper.run_concurrently`. --- Lib/test/test_free_threading/test_bisect.py | 24 +-------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Lib/test/test_free_threading/test_bisect.py b/Lib/test/test_free_threading/test_bisect.py index d1a8eab6fb10a9..99c9d46bd85cfc 100644 --- a/Lib/test/test_free_threading/test_bisect.py +++ b/Lib/test/test_free_threading/test_bisect.py @@ -19,7 +19,7 @@ def insert(data): insert_method(data, x) data = list(range(OBJECT_COUNT)) - self.run_concurrently( + threading_helper.run_concurrently( worker_func=insert, args=(data,), nthreads=NTHREADS ) if False: @@ -42,28 +42,6 @@ def is_sorted_ascending(lst): """ return all(lst[i - 1] <= lst[i] for i in range(1, len(lst))) - def run_concurrently(self, worker_func, args, nthreads): - """ - Run the worker function concurrently in multiple threads. - """ - barrier = Barrier(nthreads) - - def wrapper_func(*args): - # Wait for all threads to reach this point before proceeding. - barrier.wait() - worker_func(*args) - - with threading_helper.catch_threading_exception() as cm: - workers = ( - Thread(target=wrapper_func, args=args) - for _ in range(nthreads) - ) - with threading_helper.start_threads(workers): - pass - - # Worker threads should not raise any exceptions - self.assertIsNone(cm.exc_value) - @threading_helper.requires_working_threading() class TestPyBisect(unittest.TestCase, TestBase): From b74a7903e745522de41e9242b7f824e5b7d60b35 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 29 Jul 2025 19:15:29 -0700 Subject: [PATCH 5/5] Remove unused imports from test. --- Lib/test/test_free_threading/test_bisect.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_free_threading/test_bisect.py b/Lib/test/test_free_threading/test_bisect.py index 99c9d46bd85cfc..bd7732da035257 100644 --- a/Lib/test/test_free_threading/test_bisect.py +++ b/Lib/test/test_free_threading/test_bisect.py @@ -1,6 +1,5 @@ import unittest from test.support import import_helper, threading_helper -from threading import Thread, Barrier import random py_bisect = import_helper.import_fresh_module('bisect', blocked=['_bisect'])