From 29da66dd834d00314360e0358e171342634c5283 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 29 Jul 2025 19:44:10 -0700 Subject: [PATCH 1/2] GH-116738: document thread-safety of bisect (GH-136555) (cherry picked from commit 5236b0281b91a874b14cf15f3fdef9b7beffb22f) Co-authored-by: Neil Schemenauer --- Doc/library/bisect.rst | 10 ++++ Lib/test/test_free_threading/test_bisect.py | 56 +++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 Lib/test/test_free_threading/test_bisect.py diff --git a/Doc/library/bisect.rst b/Doc/library/bisect.rst index 78da563397b625..d02ffe469adb1a 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: 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..bd7732da035257 --- /dev/null +++ b/Lib/test/test_free_threading/test_bisect.py @@ -0,0 +1,56 @@ +import unittest +from test.support import import_helper, threading_helper +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)) + threading_helper.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))) + + +@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 10d6017504e91556717f1c844a3fc53055776d59 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 30 Jul 2025 16:57:11 +0530 Subject: [PATCH 2/2] add back run_concurrently --- Lib/test/test_free_threading/test_bisect.py | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_free_threading/test_bisect.py b/Lib/test/test_free_threading/test_bisect.py index bd7732da035257..b8645d1c8c227a 100644 --- a/Lib/test/test_free_threading/test_bisect.py +++ b/Lib/test/test_free_threading/test_bisect.py @@ -1,6 +1,7 @@ import unittest from test.support import import_helper, threading_helper import random +from threading import Thread, Barrier py_bisect = import_helper.import_fresh_module('bisect', blocked=['_bisect']) c_bisect = import_helper.import_fresh_module('bisect', fresh=['_bisect']) @@ -18,7 +19,7 @@ def insert(data): insert_method(data, x) data = list(range(OBJECT_COUNT)) - threading_helper.run_concurrently( + self.run_concurrently( worker_func=insert, args=(data,), nthreads=NTHREADS ) if False: @@ -41,6 +42,28 @@ 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):