| """ | |
| Tests for the threading module. | |
| """ | |
| import test.support | |
| from test.support import threading_helper, requires_subprocess, requires_gil_enabled | |
| from test.support import verbose, cpython_only, os_helper | |
| from test.support.import_helper import ensure_lazy_imports, import_module | |
| from test.support.script_helper import assert_python_ok, assert_python_failure, spawn_python | |
| from test.support import force_not_colorized | |
| import random | |
| import sys | |
| import _thread | |
| import threading | |
| import time | |
| import unittest | |
| import weakref | |
| import os | |
| import subprocess | |
| import signal | |
| import textwrap | |
| import traceback | |
| import warnings | |
| from unittest import mock | |
| from test import lock_tests | |
| from test import support | |
| try: | |
| from concurrent import interpreters | |
| except ImportError: | |
| interpreters = None | |
| threading_helper.requires_working_threading(module=True) | |
| # Between fork() and exec(), only async-safe functions are allowed (issues | |
| # #12316 and #11870), and fork() from a worker thread is known to trigger | |
| # problems with some operating systems (issue #3863): skip problematic tests | |
| # on platforms known to behave badly. | |
| platforms_to_skip = ('netbsd5', 'hp-ux11') | |
| def skip_unless_reliable_fork(test): | |
| if not support.has_fork_support: | |
| return unittest.skip("requires working os.fork()")(test) | |
| if sys.platform in platforms_to_skip: | |
| return unittest.skip("due to known OS bug related to thread+fork")(test) | |
| if support.HAVE_ASAN_FORK_BUG: | |
| return unittest.skip("libasan has a pthread_create() dead lock related to thread+fork")(test) | |
| if support.check_sanitizer(thread=True): | |
| return unittest.skip("TSAN doesn't support threads after fork")(test) | |
| return test | |
| def requires_subinterpreters(meth): | |
| """Decorator to skip a test if subinterpreters are not supported.""" | |
| return unittest.skipIf(interpreters is None, | |
| 'subinterpreters required')(meth) | |
| def restore_default_excepthook(testcase): | |
| testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook) | |
| threading.excepthook = threading.__excepthook__ | |
| # A trivial mutable counter. | |
| class Counter(object): | |
| def __init__(self): | |
| self.value = 0 | |
| def inc(self): | |
| self.value += 1 | |
| def dec(self): | |
| self.value -= 1 | |
| def get(self): | |
| return self.value | |
| class TestThread(threading.Thread): | |
| def __init__(self, name, testcase, sema, mutex, nrunning): | |
| threading.Thread.__init__(self, name=name) | |
| self.testcase = testcase | |
| self.sema = sema | |
| self.mutex = mutex | |
| self.nrunning = nrunning | |
| def run(self): | |
| delay = random.random() / 10000.0 | |
| if verbose: | |
| print('task %s will run for %.1f usec' % | |
| (self.name, delay * 1e6)) | |
| with self.sema: | |
| with self.mutex: | |
| self.nrunning.inc() | |
| if verbose: | |
| print(self.nrunning.get(), 'tasks are running') | |
| self.testcase.assertLessEqual(self.nrunning.get(), 3) | |
| time.sleep(delay) | |
| if verbose: | |
| print('task', self.name, 'done') | |
| with self.mutex: | |
| self.nrunning.dec() | |
| self.testcase.assertGreaterEqual(self.nrunning.get(), 0) | |
| if verbose: | |
| print('%s is finished. %d tasks are running' % | |
| (self.name, self.nrunning.get())) | |
| class BaseTestCase(unittest.TestCase): | |
| def setUp(self): | |
| self._threads = threading_helper.threading_setup() | |
| def tearDown(self): | |
| threading_helper.threading_cleanup(*self._threads) | |
| test.support.reap_children() | |
| class ThreadTests(BaseTestCase): | |
| maxDiff = 9999 | |
| @cpython_only | |
| def test_lazy_import(self): | |
| ensure_lazy_imports("threading", {"functools", "warnings"}) | |
| @cpython_only | |
| def test_name(self): | |
| def func(): pass | |
| thread = threading.Thread(name="myname1") | |
| self.assertEqual(thread.name, "myname1") | |
| # Convert int name to str | |
| thread = threading.Thread(name=123) | |
| self.assertEqual(thread.name, "123") | |
| # target name is ignored if name is specified | |
| thread = threading.Thread(target=func, name="myname2") | |
| self.assertEqual(thread.name, "myname2") | |
| with mock.patch.object(threading, '_counter', return_value=2): | |
| thread = threading.Thread(name="") | |
| self.assertEqual(thread.name, "Thread-2") | |
| with mock.patch.object(threading, '_counter', return_value=3): | |
| thread = threading.Thread() | |
| self.assertEqual(thread.name, "Thread-3") | |
| with mock.patch.object(threading, '_counter', return_value=5): | |
| thread = threading.Thread(target=func) | |
| self.assertEqual(thread.name, "Thread-5 (func)") | |
| def test_args_argument(self): | |
| # bpo-45735: Using list or tuple as *args* in constructor could | |
| # achieve the same effect. | |
| num_list = [1] | |
| num_tuple = (1,) | |
| str_list = ["str"] | |
| str_tuple = ("str",) | |
| list_in_tuple = ([1],) | |
| tuple_in_list = [(1,)] | |
| test_cases = ( | |
| (num_list, lambda arg: self.assertEqual(arg, 1)), | |
| (num_tuple, lambda arg: self.assertEqual(arg, 1)), | |
| (str_list, lambda arg: self.assertEqual(arg, "str")), | |
| (str_tuple, lambda arg: self.assertEqual(arg, "str")), | |
| (list_in_tuple, lambda arg: self.assertEqual(arg, [1])), | |
| (tuple_in_list, lambda arg: self.assertEqual(arg, (1,))) | |
| ) | |
| for args, target in test_cases: | |
| with self.subTest(target=target, args=args): | |
| t = threading.Thread(target=target, args=args) | |
| t.start() | |
| t.join() | |
| def test_lock_no_args(self): | |
| threading.Lock() # works | |
| self.assertRaises(TypeError, threading.Lock, 1) | |
| self.assertRaises(TypeError, threading.Lock, a=1) | |
| self.assertRaises(TypeError, threading.Lock, 1, 2, a=1, b=2) | |
| def test_lock_no_subclass(self): | |
| # Intentionally disallow subclasses of threading.Lock because they have | |
| # never been allowed, so why start now just because the type is public? | |
| with self.assertRaises(TypeError): | |
| class MyLock(threading.Lock): pass | |
| def test_lock_or_none(self): | |
| import types | |
| self.assertIsInstance(threading.Lock | None, types.UnionType) | |
| # Create a bunch of threads, let each do some work, wait until all are | |
| # done. | |
| def test_various_ops(self): | |
| # This takes about n/3 seconds to run (about n/3 clumps of tasks, | |
| # times about 1 second per clump). | |
| NUMTASKS = 10 | |
| # no more than 3 of the 10 can run at once | |
| sema = threading.BoundedSemaphore(value=3) | |
| mutex = threading.RLock() | |
| numrunning = Counter() | |
| threads = [] | |
| for i in range(NUMTASKS): | |
| t = TestThread("<thread %d>"%i, self, sema, mutex, numrunning) | |
| threads.append(t) | |
| self.assertIsNone(t.ident) | |
| self.assertRegex(repr(t), r'^<TestThread\(.*, initial\)>$') | |
| t.start() | |
| if hasattr(threading, 'get_native_id'): | |
| native_ids = set(t.native_id for t in threads) | {threading.get_native_id()} | |
| self.assertNotIn(None, native_ids) | |
| self.assertEqual(len(native_ids), NUMTASKS + 1) | |
| if verbose: | |
| print('waiting for all tasks to complete') | |
| for t in threads: | |
| t.join() | |
| self.assertFalse(t.is_alive()) | |
| self.assertNotEqual(t.ident, 0) | |
| self.assertIsNotNone(t.ident) | |
| self.assertRegex(repr(t), r'^<TestThread\(.*, stopped -?\d+\)>$') | |
| if verbose: | |
| print('all tasks done') | |
| self.assertEqual(numrunning.get(), 0) | |
| def test_ident_of_no_threading_threads(self): | |
| # The ident still must work for the main thread and dummy threads. | |
| self.assertIsNotNone(threading.current_thread().ident) | |
| def f(): | |
| ident.append(threading.current_thread().ident) | |
| done.set() | |
| done = threading.Event() | |
| ident = [] | |
| with threading_helper.wait_threads_exit(): | |
| tid = _thread.start_new_thread(f, ()) | |
| done.wait() | |
| self.assertEqual(ident[0], tid) | |
| # run with a small(ish) thread stack size (256 KiB) | |
| def test_various_ops_small_stack(self): | |
| if verbose: | |
| print('with 256 KiB thread stack size...') | |
| try: | |
| threading.stack_size(262144) | |
| except _thread.error: | |
| raise unittest.SkipTest( | |
| 'platform does not support changing thread stack size') | |
| self.test_various_ops() | |
| threading.stack_size(0) | |
| # run with a large thread stack size (1 MiB) | |
| def test_various_ops_large_stack(self): | |
| if verbose: | |
| print('with 1 MiB thread stack size...') | |
| try: | |
| threading.stack_size(0x100000) | |
| except _thread.error: | |
| raise unittest.SkipTest( | |
| 'platform does not support changing thread stack size') | |
| self.test_various_ops() | |
| threading.stack_size(0) | |
| def test_foreign_thread(self): | |
| # Check that a "foreign" thread can use the threading module. | |
| dummy_thread = None | |
| error = None | |
| def f(mutex): | |
| try: | |
| nonlocal dummy_thread | |
| nonlocal error | |
| # Calling current_thread() forces an entry for the foreign | |
| # thread to get made in the threading._active map. | |
| dummy_thread = threading.current_thread() | |
| tid = dummy_thread.ident | |
| self.assertIn(tid, threading._active) | |
| self.assertIsInstance(dummy_thread, threading._DummyThread) | |
| self.assertIs(threading._active.get(tid), dummy_thread) | |
| # gh-29376 | |
| self.assertTrue( | |
| dummy_thread.is_alive(), | |
| 'Expected _DummyThread to be considered alive.' | |
| ) | |
| self.assertIn('_DummyThread', repr(dummy_thread)) | |
| except BaseException as e: | |
| error = e | |
| finally: | |
| mutex.release() | |
| mutex = threading.Lock() | |
| mutex.acquire() | |
| with threading_helper.wait_threads_exit(): | |
| tid = _thread.start_new_thread(f, (mutex,)) | |
| # Wait for the thread to finish. | |
| mutex.acquire() | |
| if error is not None: | |
| raise error | |
| self.assertEqual(tid, dummy_thread.ident) | |
| # Issue gh-106236: | |
| with self.assertRaises(RuntimeError): | |
| dummy_thread.join() | |
| dummy_thread._started.clear() | |
| with self.assertRaises(RuntimeError): | |
| dummy_thread.is_alive() | |
| # Busy wait for the following condition: after the thread dies, the | |
| # related dummy thread must be removed from threading._active. | |
| timeout = 5 | |
| timeout_at = time.monotonic() + timeout | |
| while time.monotonic() < timeout_at: | |
| if threading._active.get(dummy_thread.ident) is not dummy_thread: | |
| break | |
| time.sleep(.1) | |
| else: | |
| self.fail('It was expected that the created threading._DummyThread was removed from threading._active.') | |
| # PyThreadState_SetAsyncExc() is a CPython-only gimmick, not (currently) | |
| # exposed at the Python level. This test relies on ctypes to get at it. | |
| @cpython_only | |
| def test_PyThreadState_SetAsyncExc(self): | |
| ctypes = import_module("ctypes") | |
| import ctypes.util # noqa: F811 | |
| @ctypes.util.wrap_dll_function(ctypes.pythonapi) | |
| def PyThreadState_SetAsyncExc(id: ctypes.c_ulong, | |
| exc: ctypes.py_object) -> ctypes.c_int: | |
| pass | |
| set_async_exc = PyThreadState_SetAsyncExc | |
| class AsyncExc(Exception): | |
| pass | |
| exception = ctypes.py_object(AsyncExc) | |
| # First check it works when setting the exception from the same thread. | |
| tid = threading.get_ident() | |
| self.assertIsInstance(tid, int) | |
| self.assertGreater(tid, 0) | |
| try: | |
| result = set_async_exc(tid, exception) | |
| # The exception is async, so we might have to keep the VM busy until | |
| # it notices. | |
| while True: | |
| pass | |
| except AsyncExc: | |
| pass | |
| else: | |
| # This code is unreachable but it reflects the intent. If we wanted | |
| # to be smarter the above loop wouldn't be infinite. | |
| self.fail("AsyncExc not raised") | |
| try: | |
| self.assertEqual(result, 1) # one thread state modified | |
| except UnboundLocalError: | |
| # The exception was raised too quickly for us to get the result. | |
| pass | |
| # `worker_started` is set by the thread when it's inside a try/except | |
| # block waiting to catch the asynchronously set AsyncExc exception. | |
| # `worker_saw_exception` is set by the thread upon catching that | |
| # exception. | |
| worker_started = threading.Event() | |
| worker_saw_exception = threading.Event() | |
| class Worker(threading.Thread): | |
| def run(self): | |
| self.id = threading.get_ident() | |
| self.finished = False | |
| try: | |
| while True: | |
| worker_started.set() | |
| time.sleep(0.1) | |
| except AsyncExc: | |
| self.finished = True | |
| worker_saw_exception.set() | |
| t = Worker() | |
| t.daemon = True # so if this fails, we don't hang Python at shutdown | |
| t.start() | |
| if verbose: | |
| print(" started worker thread") | |
| # Try a thread id that doesn't make sense. | |
| if verbose: | |
| print(" trying nonsensical thread id") | |
| result = set_async_exc(-1, exception) | |
| self.assertEqual(result, 0) # no thread states modified | |
| # Now raise an exception in the worker thread. | |
| if verbose: | |
| print(" waiting for worker thread to get started") | |
| ret = worker_started.wait() | |
| self.assertTrue(ret) | |
| if verbose: | |
| print(" verifying worker hasn't exited") | |
| self.assertFalse(t.finished) | |
| if verbose: | |
| print(" attempting to raise asynch exception in worker") | |
| result = set_async_exc(t.id, exception) | |
| self.assertEqual(result, 1) # one thread state modified | |
| if verbose: | |
| print(" waiting for worker to say it caught the exception") | |
| worker_saw_exception.wait(timeout=support.SHORT_TIMEOUT) | |
| self.assertTrue(t.finished) | |
| if verbose: | |
| print(" all OK -- joining worker") | |
| if t.finished: | |
| t.join() | |
| # else the thread is still running, and we have no way to kill it | |
| @cpython_only | |
| @unittest.skipUnless(hasattr(signal, "pthread_kill"), "need pthread_kill") | |
| @unittest.skipUnless(hasattr(signal, "SIGUSR1"), "need SIGUSR1") | |
| def test_PyThreadState_SetAsyncExc_interrupts_sleep(self): | |
| _testcapi = import_module("_testlimitedcapi") | |
| worker_started = threading.Event() | |
| class InjectedException(Exception): | |
| """Custom exception for testing""" | |
| caught_exception = None | |
| def catch_exception(): | |
| nonlocal caught_exception | |
| day_as_seconds = 60 * 60 * 24 | |
| try: | |
| worker_started.set() | |
| time.sleep(day_as_seconds) | |
| except InjectedException as exc: | |
| caught_exception = exc | |
| thread = threading.Thread(target=catch_exception) | |
| thread.start() | |
| worker_started.wait() | |
| signal.signal(signal.SIGUSR1, lambda sig, frame: None) | |
| result = _testcapi.threadstate_set_async_exc( | |
| thread.ident, InjectedException) | |
| self.assertEqual(result, 1) | |
| for _ in support.sleeping_retry(support.SHORT_TIMEOUT): | |
| if not thread.is_alive(): | |
| break | |
| try: | |
| signal.pthread_kill(thread.ident, signal.SIGUSR1) | |
| except OSError: | |
| # The thread might have terminated between the is_alive check | |
| # and the pthread_kill | |
| break | |
| thread.join() | |
| signal.signal(signal.SIGUSR1, signal.SIG_DFL) | |
| self.assertIsInstance(caught_exception, InjectedException) | |
| def test_limbo_cleanup(self): | |
| # Issue 7481: Failure to start thread should cleanup the limbo map. | |
| def fail_new_thread(*args, **kwargs): | |
| raise threading.ThreadError() | |
| _start_joinable_thread = threading._start_joinable_thread | |
| threading._start_joinable_thread = fail_new_thread | |
| try: | |
| t = threading.Thread(target=lambda: None) | |
| self.assertRaises(threading.ThreadError, t.start) | |
| self.assertFalse( | |
| t in threading._limbo, | |
| "Failed to cleanup _limbo map on failure of Thread.start().") | |
| finally: | |
| threading._start_joinable_thread = _start_joinable_thread | |
| def test_finalize_running_thread(self): | |
| # Issue 1402: the PyGILState_Ensure / _Release functions may be called | |
| # very late on python exit: on deallocation of a running thread for | |
| # example. | |
| if support.check_sanitizer(thread=True): | |
| # the thread running `time.sleep(100)` below will still be alive | |
| # at process exit | |
| self.skipTest("TSAN would report thread leak") | |
| import_module("ctypes") | |
| rc, out, err = assert_python_failure("-c", """if 1: | |
| import ctypes.util, sys, time, _thread | |
| PyGILState_STATE = ctypes.c_int # enum | |
| @ctypes.util.wrap_dll_function(ctypes.pythonapi) | |
| def PyGILState_Ensure() -> PyGILState_STATE: | |
| pass | |
| @ctypes.util.wrap_dll_function(ctypes.pythonapi) | |
| def PyGILState_Release(oldstate: PyGILState_STATE) -> None: | |
| pass | |
| # This lock is used as a simple event variable. | |
| ready = _thread.allocate_lock() | |
| ready.acquire() | |
| # Module globals are cleared before __del__ is run | |
| # So we save the functions in class dict | |
| class C: | |
| ensure = PyGILState_Ensure | |
| release = PyGILState_Release | |
| def __del__(self): | |
| state = self.ensure() | |
| self.release(state) | |
| def waitingThread(): | |
| x = C() | |
| ready.release() | |
| time.sleep(100) | |
| _thread.start_new_thread(waitingThread, ()) | |
| ready.acquire() # Be sure the other thread is waiting. | |
| sys.exit(42) | |
| """) | |
| self.assertEqual(rc, 42) | |
| def test_finalize_with_trace(self): | |
| # Issue1733757 | |
| # Avoid a deadlock when sys.settrace steps into threading._shutdown | |
| if support.check_sanitizer(thread=True): | |
| # the thread running `time.sleep(2)` below will still be alive | |
| # at process exit | |
| self.skipTest("TSAN would report thread leak") | |
| assert_python_ok("-c", """if 1: | |
| import sys, threading | |
| # A deadlock-killer, to prevent the | |
| # testsuite to hang forever | |
| def killer(): | |
| import os, time | |
| time.sleep(2) | |
| print('program blocked; aborting') | |
| os._exit(2) | |
| t = threading.Thread(target=killer) | |
| t.daemon = True | |
| t.start() | |
| # This is the trace function | |
| def func(frame, event, arg): | |
| threading.current_thread() | |
| return func | |
| sys.settrace(func) | |
| """) | |
| def test_join_nondaemon_on_shutdown(self): | |
| # Issue 1722344 | |
| # Raising SystemExit skipped threading._shutdown | |
| rc, out, err = assert_python_ok("-c", """if 1: | |
| import threading | |
| from time import sleep | |
| def child(): | |
| sleep(1) | |
| # As a non-daemon thread we SHOULD wake up and nothing | |
| # should be torn down yet | |
| print("Woke up, sleep function is:", sleep) | |
| threading.Thread(target=child).start() | |
| raise SystemExit | |
| """) | |
| self.assertEqual(out.strip(), | |
| b"Woke up, sleep function is: <built-in function sleep>") | |
| self.assertEqual(err, b"") | |
| def test_enumerate_after_join(self): | |
| # Try hard to trigger #1703448: a thread is still returned in | |
| # threading.enumerate() after it has been join()ed. | |
| enum = threading.enumerate | |
| old_interval = sys.getswitchinterval() | |
| try: | |
| for i in range(1, 100): | |
| support.setswitchinterval(i * 0.0002) | |
| t = threading.Thread(target=lambda: None) | |
| t.start() | |
| t.join() | |
| l = enum() | |
| self.assertNotIn(t, l, | |
| "#1703448 triggered after %d trials: %s" % (i, l)) | |
| finally: | |
| sys.setswitchinterval(old_interval) | |
| @support.bigmemtest(size=20, memuse=72*2**20, dry_run=False) | |
| def test_join_from_multiple_threads(self, size): | |
| # Thread.join() should be thread-safe | |
| errors = [] | |
| def worker(): | |
| time.sleep(0.005) | |
| def joiner(thread): | |
| try: | |
| thread.join() | |
| except Exception as e: | |
| errors.append(e) | |
| for N in range(2, 20): | |
| threads = [threading.Thread(target=worker)] | |
| for i in range(N): | |
| threads.append(threading.Thread(target=joiner, | |
| args=(threads[0],))) | |
| for t in threads: | |
| t.start() | |
| time.sleep(0.01) | |
| for t in threads: | |
| t.join() | |
| if errors: | |
| raise errors[0] | |
| def test_join_with_timeout(self): | |
| lock = _thread.allocate_lock() | |
| lock.acquire() | |
| def worker(): | |
| lock.acquire() | |
| thread = threading.Thread(target=worker) | |
| thread.start() | |
| thread.join(timeout=0.01) | |
| assert thread.is_alive() | |
| lock.release() | |
| thread.join() | |
| assert not thread.is_alive() | |
| def test_no_refcycle_through_target(self): | |
| class RunSelfFunction(object): | |
| def __init__(self, should_raise): | |
| # The links in this refcycle from Thread back to self | |
| # should be cleaned up when the thread completes. | |
| self.should_raise = should_raise | |
| self.thread = threading.Thread(target=self._run, | |
| args=(self,), | |
| kwargs={'yet_another':self}) | |
| self.thread.start() | |
| def _run(self, other_ref, yet_another): | |
| if self.should_raise: | |
| raise SystemExit | |
| restore_default_excepthook(self) | |
| cyclic_object = RunSelfFunction(should_raise=False) | |
| weak_cyclic_object = weakref.ref(cyclic_object) | |
| cyclic_object.thread.join() | |
| del cyclic_object | |
| self.assertIsNone(weak_cyclic_object(), | |
| msg=('%d references still around' % | |
| sys.getrefcount(weak_cyclic_object()))) | |
| raising_cyclic_object = RunSelfFunction(should_raise=True) | |
| weak_raising_cyclic_object = weakref.ref(raising_cyclic_object) | |
| raising_cyclic_object.thread.join() | |
| del raising_cyclic_object | |
| self.assertIsNone(weak_raising_cyclic_object(), | |
| msg=('%d references still around' % | |
| sys.getrefcount(weak_raising_cyclic_object()))) | |
| def test_old_threading_api(self): | |
| # Just a quick sanity check to make sure the old method names are | |
| # still present | |
| t = threading.Thread() | |
| with self.assertWarnsRegex(DeprecationWarning, | |
| r'get the daemon attribute'): | |
| t.isDaemon() | |
| with self.assertWarnsRegex(DeprecationWarning, | |
| r'set the daemon attribute'): | |
| t.setDaemon(True) | |
| with self.assertWarnsRegex(DeprecationWarning, | |
| r'get the name attribute'): | |
| t.getName() | |
| with self.assertWarnsRegex(DeprecationWarning, | |
| r'set the name attribute'): | |
| t.setName("name") | |
| e = threading.Event() | |
| with self.assertWarnsRegex(DeprecationWarning, 'use is_set()'): | |
| e.isSet() | |
| cond = threading.Condition() | |
| cond.acquire() | |
| with self.assertWarnsRegex(DeprecationWarning, 'use notify_all()'): | |
| cond.notifyAll() | |
| with self.assertWarnsRegex(DeprecationWarning, 'use active_count()'): | |
| threading.activeCount() | |
| with self.assertWarnsRegex(DeprecationWarning, 'use current_thread()'): | |
| threading.currentThread() | |
| def test_repr_daemon(self): | |
| t = threading.Thread() | |
| self.assertNotIn('daemon', repr(t)) | |
| t.daemon = True | |
| self.assertIn('daemon', repr(t)) | |
| def test_daemon_param(self): | |
| t = threading.Thread() | |
| self.assertFalse(t.daemon) | |
| t = threading.Thread(daemon=False) | |
| self.assertFalse(t.daemon) | |
| t = threading.Thread(daemon=True) | |
| self.assertTrue(t.daemon) | |
| @skip_unless_reliable_fork | |
| def test_dummy_thread_after_fork(self): | |
| # Issue #14308: a dummy thread in the active list doesn't mess up | |
| # the after-fork mechanism. | |
| code = """if 1: | |
| import _thread, threading, os, time, warnings | |
| def background_thread(evt): | |
| # Creates and registers the _DummyThread instance | |
| threading.current_thread() | |
| evt.set() | |
| time.sleep(10) | |
| evt = threading.Event() | |
| _thread.start_new_thread(background_thread, (evt,)) | |
| evt.wait() | |
| assert threading.active_count() == 2, threading.active_count() | |
| with warnings.catch_warnings(record=True) as ws: | |
| warnings.filterwarnings( | |
| "always", category=DeprecationWarning) | |
| if os.fork() == 0: | |
| assert threading.active_count() == 1, threading.active_count() | |
| os._exit(0) | |
| else: | |
| assert ws[0].category == DeprecationWarning, ws[0] | |
| assert 'fork' in str(ws[0].message), ws[0] | |
| os.wait() | |
| """ | |
| _, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(out, b'') | |
| self.assertEqual(err, b'') | |
| @skip_unless_reliable_fork | |
| def test_is_alive_after_fork(self): | |
| # Try hard to trigger #18418: is_alive() could sometimes be True on | |
| # threads that vanished after a fork. | |
| old_interval = sys.getswitchinterval() | |
| self.addCleanup(sys.setswitchinterval, old_interval) | |
| # Make the bug more likely to manifest. | |
| test.support.setswitchinterval(1e-6) | |
| for i in range(20): | |
| t = threading.Thread(target=lambda: None) | |
| t.start() | |
| # Ignore the warning about fork with threads. | |
| with warnings.catch_warnings(category=DeprecationWarning, | |
| action="ignore"): | |
| if (pid := os.fork()) == 0: | |
| os._exit(11 if t.is_alive() else 10) | |
| else: | |
| t.join() | |
| support.wait_process(pid, exitcode=10) | |
| def test_main_thread(self): | |
| main = threading.main_thread() | |
| self.assertEqual(main.name, 'MainThread') | |
| self.assertEqual(main.ident, threading.current_thread().ident) | |
| self.assertEqual(main.ident, threading.get_ident()) | |
| def f(): | |
| self.assertNotEqual(threading.main_thread().ident, | |
| threading.current_thread().ident) | |
| th = threading.Thread(target=f) | |
| th.start() | |
| th.join() | |
| @skip_unless_reliable_fork | |
| @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") | |
| def test_main_thread_after_fork(self): | |
| code = """if 1: | |
| import os, threading | |
| from test import support | |
| ident = threading.get_ident() | |
| pid = os.fork() | |
| if pid == 0: | |
| print("current ident", threading.get_ident() == ident) | |
| main = threading.main_thread() | |
| print("main", main.name) | |
| print("main ident", main.ident == ident) | |
| print("current is main", threading.current_thread() is main) | |
| else: | |
| support.wait_process(pid, exitcode=0) | |
| """ | |
| _, out, err = assert_python_ok("-c", code) | |
| data = out.decode().replace('\r', '') | |
| self.assertEqual(err, b"") | |
| self.assertEqual(data, | |
| "current ident True\n" | |
| "main MainThread\n" | |
| "main ident True\n" | |
| "current is main True\n") | |
| @skip_unless_reliable_fork | |
| @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") | |
| def test_main_thread_after_fork_from_nonmain_thread(self): | |
| code = """if 1: | |
| import os, threading, sys, warnings | |
| from test import support | |
| def func(): | |
| ident = threading.get_ident() | |
| with warnings.catch_warnings(record=True) as ws: | |
| warnings.filterwarnings( | |
| "always", category=DeprecationWarning) | |
| pid = os.fork() | |
| if pid == 0: | |
| print("current ident", threading.get_ident() == ident) | |
| main = threading.main_thread() | |
| print("main", main.name, type(main).__name__) | |
| print("main ident", main.ident == ident) | |
| print("current is main", threading.current_thread() is main) | |
| # stdout is fully buffered because not a tty, | |
| # we have to flush before exit. | |
| sys.stdout.flush() | |
| else: | |
| assert ws[0].category == DeprecationWarning, ws[0] | |
| assert 'fork' in str(ws[0].message), ws[0] | |
| support.wait_process(pid, exitcode=0) | |
| th = threading.Thread(target=func) | |
| th.start() | |
| th.join() | |
| """ | |
| _, out, err = assert_python_ok("-c", code) | |
| data = out.decode().replace('\r', '') | |
| self.assertEqual(err.decode('utf-8'), "") | |
| self.assertEqual(data, | |
| "current ident True\n" | |
| "main Thread-1 (func) Thread\n" | |
| "main ident True\n" | |
| "current is main True\n" | |
| ) | |
| @skip_unless_reliable_fork | |
| @unittest.skipUnless(hasattr(os, 'waitpid'), "test needs os.waitpid()") | |
| def test_main_thread_after_fork_from_foreign_thread(self, create_dummy=False): | |
| code = """if 1: | |
| import os, threading, sys, traceback, _thread | |
| from test import support | |
| def func(lock): | |
| ident = threading.get_ident() | |
| if %s: | |
| # call current_thread() before fork to allocate DummyThread | |
| current = threading.current_thread() | |
| print("current", current.name, type(current).__name__) | |
| print("ident in _active", ident in threading._active) | |
| # flush before fork, so child won't flush it again | |
| sys.stdout.flush() | |
| pid = os.fork() | |
| if pid == 0: | |
| print("current ident", threading.get_ident() == ident) | |
| main = threading.main_thread() | |
| print("main", main.name, type(main).__name__) | |
| print("main ident", main.ident == ident) | |
| print("current is main", threading.current_thread() is main) | |
| print("_dangling", [t.name for t in list(threading._dangling)]) | |
| # stdout is fully buffered because not a tty, | |
| # we have to flush before exit. | |
| sys.stdout.flush() | |
| try: | |
| threading._shutdown() | |
| os._exit(0) | |
| except: | |
| traceback.print_exc() | |
| sys.stderr.flush() | |
| os._exit(1) | |
| else: | |
| try: | |
| support.wait_process(pid, exitcode=0) | |
| except Exception: | |
| # avoid 'could not acquire lock for | |
| # <_io.BufferedWriter name='<stderr>'> at interpreter shutdown,' | |
| traceback.print_exc() | |
| sys.stderr.flush() | |
| finally: | |
| lock.release() | |
| join_lock = _thread.allocate_lock() | |
| join_lock.acquire() | |
| th = _thread.start_new_thread(func, (join_lock,)) | |
| join_lock.acquire() | |
| """ % create_dummy | |
| # "DeprecationWarning: This process is multi-threaded, use of fork() | |
| # may lead to deadlocks in the child" | |
| _, out, err = assert_python_ok("-W", "ignore::DeprecationWarning", "-c", code) | |
| data = out.decode().replace('\r', '') | |
| self.assertEqual(err.decode(), "") | |
| self.assertEqual(data, | |
| ("current Dummy-1 _DummyThread\n" if create_dummy else "") + | |
| f"ident in _active {create_dummy!s}\n" + | |
| "current ident True\n" | |
| "main MainThread _MainThread\n" | |
| "main ident True\n" | |
| "current is main True\n" | |
| "_dangling ['MainThread']\n") | |
| def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False): | |
| self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True) | |
| def test_main_thread_during_shutdown(self): | |
| # bpo-31516: current_thread() should still point to the main thread | |
| # at shutdown | |
| code = """if 1: | |
| import gc, threading | |
| main_thread = threading.current_thread() | |
| assert main_thread is threading.main_thread() # sanity check | |
| class RefCycle: | |
| def __init__(self): | |
| self.cycle = self | |
| def __del__(self): | |
| print("GC:", | |
| threading.current_thread() is main_thread, | |
| threading.main_thread() is main_thread, | |
| threading.enumerate() == [main_thread]) | |
| RefCycle() | |
| gc.collect() # sanity check | |
| x = RefCycle() | |
| """ | |
| _, out, err = assert_python_ok("-c", code) | |
| data = out.decode() | |
| self.assertEqual(err, b"") | |
| self.assertEqual(data.splitlines(), | |
| ["GC: True True True"] * 2) | |
| def test_finalization_shutdown(self): | |
| # bpo-36402: Py_Finalize() calls threading._shutdown() which must wait | |
| # until Python thread states of all non-daemon threads get deleted. | |
| # | |
| # Test similar to SubinterpThreadingTests.test_threads_join_2(), but | |
| # test the finalization of the main interpreter. | |
| code = """if 1: | |
| import os | |
| import threading | |
| import time | |
| import random | |
| def random_sleep(): | |
| seconds = random.random() * 0.010 | |
| time.sleep(seconds) | |
| class Sleeper: | |
| def __del__(self): | |
| random_sleep() | |
| tls = threading.local() | |
| def f(): | |
| # Sleep a bit so that the thread is still running when | |
| # Py_Finalize() is called. | |
| random_sleep() | |
| tls.x = Sleeper() | |
| random_sleep() | |
| threading.Thread(target=f).start() | |
| random_sleep() | |
| """ | |
| rc, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(err, b"") | |
| def test_repr_stopped(self): | |
| # Verify that "stopped" shows up in repr(Thread) appropriately. | |
| started = _thread.allocate_lock() | |
| finish = _thread.allocate_lock() | |
| started.acquire() | |
| finish.acquire() | |
| def f(): | |
| started.release() | |
| finish.acquire() | |
| t = threading.Thread(target=f) | |
| t.start() | |
| started.acquire() | |
| self.assertIn("started", repr(t)) | |
| finish.release() | |
| # "stopped" should appear in the repr in a reasonable amount of time. | |
| # Implementation detail: as of this writing, that's trivially true | |
| # if .join() is called, and almost trivially true if .is_alive() is | |
| # called. The detail we're testing here is that "stopped" shows up | |
| # "all on its own". | |
| LOOKING_FOR = "stopped" | |
| for i in range(500): | |
| if LOOKING_FOR in repr(t): | |
| break | |
| time.sleep(0.01) | |
| self.assertIn(LOOKING_FOR, repr(t)) # we waited at least 5 seconds | |
| t.join() | |
| def test_BoundedSemaphore_limit(self): | |
| # BoundedSemaphore should raise ValueError if released too often. | |
| for limit in range(1, 10): | |
| bs = threading.BoundedSemaphore(limit) | |
| threads = [threading.Thread(target=bs.acquire) | |
| for _ in range(limit)] | |
| for t in threads: | |
| t.start() | |
| for t in threads: | |
| t.join() | |
| threads = [threading.Thread(target=bs.release) | |
| for _ in range(limit)] | |
| for t in threads: | |
| t.start() | |
| for t in threads: | |
| t.join() | |
| self.assertRaises(ValueError, bs.release) | |
| @cpython_only | |
| def test_frame_tstate_tracing(self): | |
| _testcapi = import_module("_testcapi") | |
| # Issue #14432: Crash when a generator is created in a C thread that is | |
| # destroyed while the generator is still used. The issue was that a | |
| # generator contains a frame, and the frame kept a reference to the | |
| # Python state of the destroyed C thread. The crash occurs when a trace | |
| # function is setup. | |
| def noop_trace(frame, event, arg): | |
| # no operation | |
| return noop_trace | |
| def generator(): | |
| while 1: | |
| yield "generator" | |
| def callback(): | |
| if callback.gen is None: | |
| callback.gen = generator() | |
| return next(callback.gen) | |
| callback.gen = None | |
| old_trace = sys.gettrace() | |
| sys.settrace(noop_trace) | |
| try: | |
| # Install a trace function | |
| threading.settrace(noop_trace) | |
| # Create a generator in a C thread which exits after the call | |
| _testcapi.call_in_temporary_c_thread(callback) | |
| # Call the generator in a different Python thread, check that the | |
| # generator didn't keep a reference to the destroyed thread state | |
| for test in range(3): | |
| # The trace function is still called here | |
| callback() | |
| finally: | |
| sys.settrace(old_trace) | |
| threading.settrace(old_trace) | |
| def test_gettrace(self): | |
| def noop_trace(frame, event, arg): | |
| # no operation | |
| return noop_trace | |
| old_trace = threading.gettrace() | |
| try: | |
| threading.settrace(noop_trace) | |
| trace_func = threading.gettrace() | |
| self.assertEqual(noop_trace,trace_func) | |
| finally: | |
| threading.settrace(old_trace) | |
| def test_gettrace_all_threads(self): | |
| def fn(*args): pass | |
| old_trace = threading.gettrace() | |
| first_check = threading.Event() | |
| second_check = threading.Event() | |
| trace_funcs = [] | |
| def checker(): | |
| trace_funcs.append(sys.gettrace()) | |
| first_check.set() | |
| second_check.wait() | |
| trace_funcs.append(sys.gettrace()) | |
| try: | |
| t = threading.Thread(target=checker) | |
| t.start() | |
| first_check.wait() | |
| threading.settrace_all_threads(fn) | |
| second_check.set() | |
| t.join() | |
| self.assertEqual(trace_funcs, [None, fn]) | |
| self.assertEqual(threading.gettrace(), fn) | |
| self.assertEqual(sys.gettrace(), fn) | |
| finally: | |
| threading.settrace_all_threads(old_trace) | |
| self.assertEqual(threading.gettrace(), old_trace) | |
| self.assertEqual(sys.gettrace(), old_trace) | |
| def test_getprofile(self): | |
| def fn(*args): pass | |
| old_profile = threading.getprofile() | |
| try: | |
| threading.setprofile(fn) | |
| self.assertEqual(fn, threading.getprofile()) | |
| finally: | |
| threading.setprofile(old_profile) | |
| def test_getprofile_all_threads(self): | |
| def fn(*args): pass | |
| old_profile = threading.getprofile() | |
| first_check = threading.Event() | |
| second_check = threading.Event() | |
| profile_funcs = [] | |
| def checker(): | |
| profile_funcs.append(sys.getprofile()) | |
| first_check.set() | |
| second_check.wait() | |
| profile_funcs.append(sys.getprofile()) | |
| try: | |
| t = threading.Thread(target=checker) | |
| t.start() | |
| first_check.wait() | |
| threading.setprofile_all_threads(fn) | |
| second_check.set() | |
| t.join() | |
| self.assertEqual(profile_funcs, [None, fn]) | |
| self.assertEqual(threading.getprofile(), fn) | |
| self.assertEqual(sys.getprofile(), fn) | |
| finally: | |
| threading.setprofile_all_threads(old_profile) | |
| self.assertEqual(threading.getprofile(), old_profile) | |
| self.assertEqual(sys.getprofile(), old_profile) | |
| def test_locals_at_exit(self): | |
| # bpo-19466: thread locals must not be deleted before destructors | |
| # are called | |
| rc, out, err = assert_python_ok("-c", """if 1: | |
| import threading | |
| class Atexit: | |
| def __del__(self): | |
| print("thread_dict.atexit = %r" % thread_dict.atexit) | |
| thread_dict = threading.local() | |
| thread_dict.atexit = "value" | |
| atexit = Atexit() | |
| """) | |
| self.assertEqual(out.rstrip(), b"thread_dict.atexit = 'value'") | |
| def test_boolean_target(self): | |
| # bpo-41149: A thread that had a boolean value of False would not | |
| # run, regardless of whether it was callable. The correct behaviour | |
| # is for a thread to do nothing if its target is None, and to call | |
| # the target otherwise. | |
| class BooleanTarget(object): | |
| def __init__(self): | |
| self.ran = False | |
| def __bool__(self): | |
| return False | |
| def __call__(self): | |
| self.ran = True | |
| target = BooleanTarget() | |
| thread = threading.Thread(target=target) | |
| thread.start() | |
| thread.join() | |
| self.assertTrue(target.ran) | |
| def test_leak_without_join(self): | |
| # bpo-37788: Test that a thread which is not joined explicitly | |
| # does not leak. Test written for reference leak checks. | |
| def noop(): pass | |
| with threading_helper.wait_threads_exit(): | |
| threading.Thread(target=noop).start() | |
| # Thread.join() is not called | |
| def test_import_from_another_thread(self): | |
| # bpo-1596321: If the threading module is first import from a thread | |
| # different than the main thread, threading._shutdown() must handle | |
| # this case without logging an error at Python exit. | |
| code = textwrap.dedent(''' | |
| import _thread | |
| import sys | |
| event = _thread.allocate_lock() | |
| event.acquire() | |
| def import_threading(): | |
| import threading | |
| event.release() | |
| if 'threading' in sys.modules: | |
| raise Exception('threading is already imported') | |
| _thread.start_new_thread(import_threading, ()) | |
| # wait until the threading module is imported | |
| event.acquire() | |
| event.release() | |
| if 'threading' not in sys.modules: | |
| raise Exception('threading is not imported') | |
| # don't wait until the thread completes | |
| ''') | |
| rc, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(out, b'') | |
| self.assertEqual(err, b'') | |
| def test_start_new_thread_at_finalization(self): | |
| code = """if 1: | |
| import _thread | |
| def f(): | |
| print("shouldn't be printed") | |
| class AtFinalization: | |
| def __del__(self): | |
| print("OK") | |
| _thread.start_new_thread(f, ()) | |
| at_finalization = AtFinalization() | |
| """ | |
| _, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(out.strip(), b"OK") | |
| self.assertIn(b"can't create new thread at interpreter shutdown", err) | |
| def test_join_daemon_thread_in_finalization(self): | |
| # gh-123940: Py_Finalize() prevents other threads from running Python | |
| # code, so join() can not succeed unless the thread is already done. | |
| # (Non-Python threads, that is `threading._DummyThread`, can't be | |
| # joined at all.) | |
| # We raise an exception rather than hang. | |
| for timeout in (None, 10): | |
| with self.subTest(timeout=timeout): | |
| code = textwrap.dedent(f""" | |
| import threading | |
| def loop(): | |
| while True: | |
| pass | |
| class Cycle: | |
| def __init__(self): | |
| self.self_ref = self | |
| self.thr = threading.Thread( | |
| target=loop, daemon=True) | |
| self.thr.start() | |
| def __del__(self): | |
| assert self.thr.is_alive() | |
| try: | |
| self.thr.join(timeout={timeout}) | |
| except PythonFinalizationError: | |
| assert self.thr.is_alive() | |
| print('got the correct exception!') | |
| # Cycle holds a reference to itself, which ensures it is | |
| # cleaned up during the GC that runs after daemon threads | |
| # have been forced to exit during finalization. | |
| Cycle() | |
| """) | |
| rc, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(err, b"") | |
| self.assertIn(b"got the correct exception", out) | |
| def test_join_finished_daemon_thread_in_finalization(self): | |
| # (see previous test) | |
| # If the thread is already finished, join() succeeds. | |
| code = textwrap.dedent(""" | |
| import threading | |
| done = threading.Event() | |
| def set_event(): | |
| done.set() | |
| class Cycle: | |
| def __init__(self): | |
| self.self_ref = self | |
| self.thr = threading.Thread(target=set_event, daemon=True) | |
| self.thr.start() | |
| self.thr.join() | |
| def __del__(self): | |
| assert done.is_set() | |
| assert not self.thr.is_alive() | |
| self.thr.join() | |
| assert not self.thr.is_alive() | |
| print('all clear!') | |
| Cycle() | |
| """) | |
| rc, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(err, b"") | |
| self.assertIn(b"all clear", out) | |
| @support.subTests('lock_class_name', ['Lock', 'RLock']) | |
| def test_acquire_daemon_thread_lock_in_finalization(self, lock_class_name): | |
| # gh-123940: Py_Finalize() prevents other threads from running Python | |
| # code (and so, releasing locks), so acquiring a locked lock can not | |
| # succeed. | |
| # We raise an exception rather than hang. | |
| code = textwrap.dedent(f""" | |
| import threading | |
| import time | |
| thread_started_event = threading.Event() | |
| lock = threading.{lock_class_name}() | |
| def loop(): | |
| if {lock_class_name!r} == 'RLock': | |
| lock.acquire() | |
| with lock: | |
| thread_started_event.set() | |
| while True: | |
| time.sleep(1) | |
| uncontested_lock = threading.{lock_class_name}() | |
| class Cycle: | |
| def __init__(self): | |
| self.self_ref = self | |
| self.thr = threading.Thread( | |
| target=loop, daemon=True) | |
| self.thr.start() | |
| thread_started_event.wait() | |
| def __del__(self): | |
| assert self.thr.is_alive() | |
| # We *can* acquire an unlocked lock | |
| uncontested_lock.acquire() | |
| if {lock_class_name!r} == 'RLock': | |
| uncontested_lock.acquire() | |
| # Acquiring a locked one fails | |
| try: | |
| lock.acquire() | |
| except PythonFinalizationError: | |
| assert self.thr.is_alive() | |
| print('got the correct exception!') | |
| # Cycle holds a reference to itself, which ensures it is | |
| # cleaned up during the GC that runs after daemon threads | |
| # have been forced to exit during finalization. | |
| Cycle() | |
| """) | |
| rc, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(err, b"") | |
| self.assertIn(b"got the correct exception", out) | |
| def test_start_new_thread_failed(self): | |
| # gh-109746: if Python fails to start newly created thread | |
| # due to failure of underlying PyThread_start_new_thread() call, | |
| # its state should be removed from interpreter' thread states list | |
| # to avoid its double cleanup | |
| try: | |
| from resource import setrlimit, RLIMIT_NPROC # noqa: F401 | |
| except ImportError as err: | |
| self.skipTest(err) # RLIMIT_NPROC is specific to Linux and BSD | |
| code = """if 1: | |
| import resource | |
| import _thread | |
| def f(): | |
| print("shouldn't be printed") | |
| limits = resource.getrlimit(resource.RLIMIT_NPROC) | |
| [_, hard] = limits | |
| resource.setrlimit(resource.RLIMIT_NPROC, (0, hard)) | |
| try: | |
| handle = _thread.start_joinable_thread(f) | |
| except RuntimeError: | |
| print('ok') | |
| else: | |
| print('!skip!') | |
| handle.join() | |
| """ | |
| _, out, err = assert_python_ok("-u", "-c", code) | |
| out = out.strip() | |
| if b'!skip!' in out: | |
| self.skipTest('RLIMIT_NPROC had no effect; probably superuser') | |
| self.assertEqual(out, b'ok') | |
| self.assertEqual(err, b'') | |
| @cpython_only | |
| def test_finalize_daemon_thread_hang(self): | |
| # gh-87135: tests that daemon threads hang during finalization | |
| script = textwrap.dedent(''' | |
| import os | |
| import sys | |
| import threading | |
| import time | |
| import _testcapi | |
| lock = threading.Lock() | |
| lock.acquire() | |
| thread_started_event = threading.Event() | |
| def thread_func(): | |
| try: | |
| thread_started_event.set() | |
| _testcapi.finalize_thread_hang(lock.acquire) | |
| finally: | |
| # Control must not reach here. | |
| os._exit(2) | |
| t = threading.Thread(target=thread_func) | |
| t.daemon = True | |
| t.start() | |
| thread_started_event.wait() | |
| # Sleep to ensure daemon thread is blocked on `lock.acquire` | |
| # | |
| # Note: This test is designed so that in the unlikely case that | |
| # `0.1` seconds is not sufficient time for the thread to become | |
| # blocked on `lock.acquire`, the test will still pass, it just | |
| # won't be properly testing the thread behavior during | |
| # finalization. | |
| time.sleep(0.1) | |
| def run_during_finalization(): | |
| # Wake up daemon thread | |
| lock.release() | |
| # Sleep to give the daemon thread time to crash if it is going | |
| # to. | |
| # | |
| # Note: If due to an exceptionally slow execution this delay is | |
| # insufficient, the test will still pass but will simply be | |
| # ineffective as a test. | |
| time.sleep(0.1) | |
| # If control reaches here, the test succeeded. | |
| os._exit(0) | |
| # Replace sys.stderr.flush as a way to run code during finalization | |
| orig_flush = sys.stderr.flush | |
| def do_flush(*args, **kwargs): | |
| orig_flush(*args, **kwargs) | |
| if not sys.is_finalizing: | |
| return | |
| sys.stderr.flush = orig_flush | |
| run_during_finalization() | |
| sys.stderr.flush = do_flush | |
| # If the follow exit code is retained, `run_during_finalization` | |
| # did not run. | |
| sys.exit(1) | |
| ''') | |
| assert_python_ok("-c", script) | |
| @skip_unless_reliable_fork | |
| @unittest.skipUnless(hasattr(threading, 'get_native_id'), "test needs threading.get_native_id()") | |
| def test_native_id_after_fork(self): | |
| script = """if True: | |
| import threading | |
| import os | |
| from test import support | |
| parent_thread_native_id = threading.current_thread().native_id | |
| print(parent_thread_native_id, flush=True) | |
| assert parent_thread_native_id == threading.get_native_id() | |
| childpid = os.fork() | |
| if childpid == 0: | |
| print(threading.current_thread().native_id, flush=True) | |
| assert threading.current_thread().native_id == threading.get_native_id() | |
| else: | |
| try: | |
| assert parent_thread_native_id == threading.current_thread().native_id | |
| assert parent_thread_native_id == threading.get_native_id() | |
| finally: | |
| support.wait_process(childpid, exitcode=0) | |
| """ | |
| rc, out, err = assert_python_ok('-c', script) | |
| self.assertEqual(rc, 0) | |
| self.assertEqual(err, b"") | |
| native_ids = out.strip().splitlines() | |
| self.assertEqual(len(native_ids), 2) | |
| self.assertNotEqual(native_ids[0], native_ids[1]) | |
| def test_stop_the_world_during_finalization(self): | |
| # gh-137433: Test functions that trigger a stop-the-world in the free | |
| # threading build concurrent with interpreter finalization. | |
| script = """if True: | |
| import gc | |
| import sys | |
| import threading | |
| NUM_THREADS = 5 | |
| b = threading.Barrier(NUM_THREADS + 1) | |
| def run_in_bg(): | |
| b.wait() | |
| while True: | |
| sys.setprofile(None) | |
| gc.collect() | |
| for _ in range(NUM_THREADS): | |
| t = threading.Thread(target=run_in_bg, daemon=True) | |
| t.start() | |
| b.wait() | |
| print("Exiting...") | |
| """ | |
| rc, out, err = assert_python_ok('-c', script) | |
| self.assertEqual(rc, 0) | |
| self.assertEqual(err, b"") | |
| self.assertEqual(out.strip(), b"Exiting...") | |
| class ThreadJoinOnShutdown(BaseTestCase): | |
| def _run_and_join(self, script): | |
| script = """if 1: | |
| import sys, os, time, threading | |
| # a thread, which waits for the main program to terminate | |
| def joiningfunc(mainthread): | |
| mainthread.join() | |
| print('end of thread') | |
| # stdout is fully buffered because not a tty, we have to flush | |
| # before exit. | |
| sys.stdout.flush() | |
| \n""" + script | |
| rc, out, err = assert_python_ok("-c", script) | |
| data = out.decode().replace('\r', '') | |
| self.assertEqual(data, "end of main\nend of thread\n") | |
| def test_1_join_on_shutdown(self): | |
| # The usual case: on exit, wait for a non-daemon thread | |
| script = """if 1: | |
| import os | |
| t = threading.Thread(target=joiningfunc, | |
| args=(threading.current_thread(),)) | |
| t.start() | |
| time.sleep(0.1) | |
| print('end of main') | |
| """ | |
| self._run_and_join(script) | |
| @skip_unless_reliable_fork | |
| def test_2_join_in_forked_process(self): | |
| # Like the test above, but from a forked interpreter | |
| script = """if 1: | |
| from test import support | |
| childpid = os.fork() | |
| if childpid != 0: | |
| # parent process | |
| support.wait_process(childpid, exitcode=0) | |
| sys.exit(0) | |
| # child process | |
| t = threading.Thread(target=joiningfunc, | |
| args=(threading.current_thread(),)) | |
| t.start() | |
| print('end of main') | |
| """ | |
| self._run_and_join(script) | |
| @skip_unless_reliable_fork | |
| def test_3_join_in_forked_from_thread(self): | |
| # Like the test above, but fork() was called from a worker thread | |
| # In the forked process, the main Thread object must be marked as stopped. | |
| script = """if 1: | |
| from test import support | |
| main_thread = threading.current_thread() | |
| def worker(): | |
| childpid = os.fork() | |
| if childpid != 0: | |
| # parent process | |
| support.wait_process(childpid, exitcode=0) | |
| sys.exit(0) | |
| # child process | |
| t = threading.Thread(target=joiningfunc, | |
| args=(main_thread,)) | |
| print('end of main') | |
| t.start() | |
| t.join() # Should not block: main_thread is already stopped | |
| w = threading.Thread(target=worker) | |
| w.start() | |
| """ | |
| self._run_and_join(script) | |
| @unittest.skipIf(sys.platform in platforms_to_skip, "due to known OS bug") | |
| @support.bigmemtest(size=40, memuse=70*2**20, dry_run=False) | |
| def test_4_daemon_threads(self, size): | |
| # Check that a daemon thread cannot crash the interpreter on shutdown | |
| # by manipulating internal structures that are being disposed of in | |
| # the main thread. | |
| if support.check_sanitizer(thread=True): | |
| # some of the threads running `random_io` below will still be alive | |
| # at process exit | |
| self.skipTest("TSAN would report thread leak") | |
| script = """if True: | |
| import os | |
| import random | |
| import sys | |
| import time | |
| import threading | |
| thread_has_run = set() | |
| def random_io(): | |
| '''Loop for a while sleeping random tiny amounts and doing some I/O.''' | |
| import test.test_threading as mod | |
| while True: | |
| with open(mod.__file__, 'rb') as in_f: | |
| stuff = in_f.read(200) | |
| with open(os.devnull, 'wb') as null_f: | |
| null_f.write(stuff) | |
| time.sleep(random.random() / 1995) | |
| thread_has_run.add(threading.current_thread()) | |
| def main(): | |
| count = 0 | |
| for _ in range(40): | |
| new_thread = threading.Thread(target=random_io) | |
| new_thread.daemon = True | |
| new_thread.start() | |
| count += 1 | |
| while len(thread_has_run) < count: | |
| time.sleep(0.001) | |
| # Trigger process shutdown | |
| sys.exit(0) | |
| main() | |
| """ | |
| rc, out, err = assert_python_ok('-c', script) | |
| self.assertFalse(err) | |
| def test_thread_from_thread(self): | |
| script = """if True: | |
| import threading | |
| import time | |
| def thread2(): | |
| time.sleep(0.05) | |
| print("OK") | |
| def thread1(): | |
| time.sleep(0.05) | |
| t2 = threading.Thread(target=thread2) | |
| t2.start() | |
| t = threading.Thread(target=thread1) | |
| t.start() | |
| # do not join() -- the interpreter waits for non-daemon threads to | |
| # finish. | |
| """ | |
| rc, out, err = assert_python_ok('-c', script) | |
| self.assertEqual(err, b"") | |
| self.assertEqual(out.strip(), b"OK") | |
| self.assertEqual(rc, 0) | |
| @skip_unless_reliable_fork | |
| def test_reinit_tls_after_fork(self): | |
| # Issue #13817: fork() would deadlock in a multithreaded program with | |
| # the ad-hoc TLS implementation. | |
| def do_fork_and_wait(): | |
| # just fork a child process and wait it | |
| pid = os.fork() | |
| if pid > 0: | |
| support.wait_process(pid, exitcode=50) | |
| else: | |
| os._exit(50) | |
| # Ignore the warning about fork with threads. | |
| with warnings.catch_warnings(category=DeprecationWarning, | |
| action="ignore"): | |
| # start a bunch of threads that will fork() child processes | |
| threads = [] | |
| for i in range(16): | |
| t = threading.Thread(target=do_fork_and_wait) | |
| threads.append(t) | |
| t.start() | |
| for t in threads: | |
| t.join() | |
| @skip_unless_reliable_fork | |
| def test_clear_threads_states_after_fork(self): | |
| # Issue #17094: check that threads states are cleared after fork() | |
| # start a bunch of threads | |
| threads = [] | |
| for i in range(16): | |
| t = threading.Thread(target=lambda : time.sleep(0.3)) | |
| threads.append(t) | |
| t.start() | |
| try: | |
| # Ignore the warning about fork with threads. | |
| with warnings.catch_warnings(category=DeprecationWarning, | |
| action="ignore"): | |
| pid = os.fork() | |
| if pid == 0: | |
| # check that threads states have been cleared | |
| if len(sys._current_frames()) == 1: | |
| os._exit(51) | |
| else: | |
| os._exit(52) | |
| else: | |
| support.wait_process(pid, exitcode=51) | |
| finally: | |
| for t in threads: | |
| t.join() | |
| class SubinterpThreadingTests(BaseTestCase): | |
| def pipe(self): | |
| r, w = os.pipe() | |
| self.addCleanup(os.close, r) | |
| self.addCleanup(os.close, w) | |
| if hasattr(os, 'set_blocking'): | |
| os.set_blocking(r, False) | |
| return (r, w) | |
| def test_threads_join(self): | |
| # Non-daemon threads should be joined at subinterpreter shutdown | |
| # (issue #18808) | |
| r, w = self.pipe() | |
| code = textwrap.dedent(r""" | |
| import os | |
| import random | |
| import threading | |
| import time | |
| def random_sleep(): | |
| seconds = random.random() * 0.010 | |
| time.sleep(seconds) | |
| def f(): | |
| # Sleep a bit so that the thread is still running when | |
| # Py_EndInterpreter is called. | |
| random_sleep() | |
| os.write(%d, b"x") | |
| threading.Thread(target=f).start() | |
| random_sleep() | |
| """ % (w,)) | |
| ret = test.support.run_in_subinterp(code) | |
| self.assertEqual(ret, 0) | |
| # The thread was joined properly. | |
| self.assertEqual(os.read(r, 1), b"x") | |
| def test_threads_join_2(self): | |
| # Same as above, but a delay gets introduced after the thread's | |
| # Python code returned but before the thread state is deleted. | |
| # To achieve this, we register a thread-local object which sleeps | |
| # a bit when deallocated. | |
| r, w = self.pipe() | |
| code = textwrap.dedent(r""" | |
| import os | |
| import random | |
| import threading | |
| import time | |
| def random_sleep(): | |
| seconds = random.random() * 0.010 | |
| time.sleep(seconds) | |
| class Sleeper: | |
| def __del__(self): | |
| random_sleep() | |
| tls = threading.local() | |
| def f(): | |
| # Sleep a bit so that the thread is still running when | |
| # Py_EndInterpreter is called. | |
| random_sleep() | |
| tls.x = Sleeper() | |
| os.write(%d, b"x") | |
| threading.Thread(target=f).start() | |
| random_sleep() | |
| """ % (w,)) | |
| ret = test.support.run_in_subinterp(code) | |
| self.assertEqual(ret, 0) | |
| # The thread was joined properly. | |
| self.assertEqual(os.read(r, 1), b"x") | |
| @requires_subinterpreters | |
| def test_threads_join_with_no_main(self): | |
| r_interp, w_interp = self.pipe() | |
| INTERP = b'I' | |
| FINI = b'F' | |
| DONE = b'D' | |
| interp = interpreters.create() | |
| interp.exec(f"""if True: | |
| import os | |
| import threading | |
| import time | |
| done = False | |
| def notify_fini(): | |
| global done | |
| done = True | |
| os.write({w_interp}, {FINI!r}) | |
| t.join() | |
| threading._register_atexit(notify_fini) | |
| def task(): | |
| while not done: | |
| time.sleep(0.1) | |
| os.write({w_interp}, {DONE!r}) | |
| t = threading.Thread(target=task) | |
| t.start() | |
| os.write({w_interp}, {INTERP!r}) | |
| """) | |
| interp.close() | |
| self.assertEqual(os.read(r_interp, 1), INTERP) | |
| self.assertEqual(os.read(r_interp, 1), FINI) | |
| self.assertEqual(os.read(r_interp, 1), DONE) | |
| @cpython_only | |
| @support.skip_if_sanitizer(thread=True, memory=True) | |
| def test_daemon_threads_fatal_error(self): | |
| import_module("_testcapi") | |
| subinterp_code = f"""if 1: | |
| import os | |
| import threading | |
| import time | |
| def f(): | |
| # Make sure the daemon thread is still running when | |
| # Py_EndInterpreter is called. | |
| time.sleep({test.support.SHORT_TIMEOUT}) | |
| threading.Thread(target=f, daemon=True).start() | |
| """ | |
| script = r"""if 1: | |
| import _testcapi | |
| _testcapi.run_in_subinterp(%r) | |
| """ % (subinterp_code,) | |
| assert_python_ok("-c", script) | |
| def _check_allowed(self, before_start='', *, | |
| allowed=True, | |
| daemon_allowed=True, | |
| daemon=False, | |
| ): | |
| import_module("_testinternalcapi") | |
| subinterp_code = textwrap.dedent(f""" | |
| import test.support | |
| import threading | |
| def func(): | |
| print('this should not have run!') | |
| t = threading.Thread(target=func, daemon={daemon}) | |
| {before_start} | |
| t.start() | |
| """) | |
| check_multi_interp_extensions = bool(support.Py_GIL_DISABLED) | |
| script = textwrap.dedent(f""" | |
| import test.support | |
| test.support.run_in_subinterp_with_config( | |
| {subinterp_code!r}, | |
| use_main_obmalloc=True, | |
| allow_fork=True, | |
| allow_exec=True, | |
| allow_threads={allowed}, | |
| allow_daemon_threads={daemon_allowed}, | |
| check_multi_interp_extensions={check_multi_interp_extensions}, | |
| own_gil=False, | |
| ) | |
| """) | |
| with test.support.SuppressCrashReport(): | |
| _, _, err = assert_python_ok("-c", script) | |
| return err.decode() | |
| @cpython_only | |
| def test_threads_not_allowed(self): | |
| err = self._check_allowed( | |
| allowed=False, | |
| daemon_allowed=False, | |
| daemon=False, | |
| ) | |
| self.assertIn('RuntimeError', err) | |
| @cpython_only | |
| def test_daemon_threads_not_allowed(self): | |
| with self.subTest('via Thread()'): | |
| err = self._check_allowed( | |
| allowed=True, | |
| daemon_allowed=False, | |
| daemon=True, | |
| ) | |
| self.assertIn('RuntimeError', err) | |
| with self.subTest('via Thread.daemon setter'): | |
| err = self._check_allowed( | |
| 't.daemon = True', | |
| allowed=True, | |
| daemon_allowed=False, | |
| daemon=False, | |
| ) | |
| self.assertIn('RuntimeError', err) | |
| class ThreadingExceptionTests(BaseTestCase): | |
| # A RuntimeError should be raised if Thread.start() is called | |
| # multiple times. | |
| def test_start_thread_again(self): | |
| thread = threading.Thread() | |
| thread.start() | |
| self.assertRaises(RuntimeError, thread.start) | |
| thread.join() | |
| def test_joining_current_thread(self): | |
| current_thread = threading.current_thread() | |
| self.assertRaises(RuntimeError, current_thread.join); | |
| def test_joining_inactive_thread(self): | |
| thread = threading.Thread() | |
| self.assertRaises(RuntimeError, thread.join) | |
| def test_daemonize_active_thread(self): | |
| thread = threading.Thread() | |
| thread.start() | |
| self.assertRaises(RuntimeError, setattr, thread, "daemon", True) | |
| thread.join() | |
| def test_releasing_unacquired_lock(self): | |
| lock = threading.Lock() | |
| self.assertRaises(RuntimeError, lock.release) | |
| @requires_subprocess() | |
| def test_recursion_limit(self): | |
| # Issue 9670 | |
| # test that excessive recursion within a non-main thread causes | |
| # an exception rather than crashing the interpreter on platforms | |
| # like Mac OS X or FreeBSD which have small default stack sizes | |
| # for threads | |
| script = """if True: | |
| import threading | |
| def recurse(): | |
| return recurse() | |
| def outer(): | |
| try: | |
| recurse() | |
| except RecursionError: | |
| pass | |
| w = threading.Thread(target=outer) | |
| w.start() | |
| w.join() | |
| print('end of main thread') | |
| """ | |
| expected_output = "end of main thread\n" | |
| p = subprocess.Popen([sys.executable, "-c", script], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| stdout, stderr = p.communicate() | |
| data = stdout.decode().replace('\r', '') | |
| self.assertEqual(p.returncode, 0, "Unexpected error: " + stderr.decode()) | |
| self.assertEqual(data, expected_output) | |
| def test_print_exception(self): | |
| script = r"""if True: | |
| import threading | |
| import time | |
| running = False | |
| def run(): | |
| global running | |
| running = True | |
| while running: | |
| time.sleep(0.01) | |
| 1/0 | |
| t = threading.Thread(target=run) | |
| t.start() | |
| while not running: | |
| time.sleep(0.01) | |
| running = False | |
| t.join() | |
| """ | |
| rc, out, err = assert_python_ok("-c", script) | |
| self.assertEqual(out, b'') | |
| err = err.decode() | |
| self.assertIn("Exception in thread", err) | |
| self.assertIn("Traceback (most recent call last):", err) | |
| self.assertIn("ZeroDivisionError", err) | |
| self.assertNotIn("Unhandled exception", err) | |
| def test_print_exception_stderr_is_none_1(self): | |
| script = r"""if True: | |
| import sys | |
| import threading | |
| import time | |
| running = False | |
| def run(): | |
| global running | |
| running = True | |
| while running: | |
| time.sleep(0.01) | |
| 1/0 | |
| t = threading.Thread(target=run) | |
| t.start() | |
| while not running: | |
| time.sleep(0.01) | |
| sys.stderr = None | |
| running = False | |
| t.join() | |
| """ | |
| rc, out, err = assert_python_ok("-c", script) | |
| self.assertEqual(out, b'') | |
| err = err.decode() | |
| self.assertIn("Exception in thread", err) | |
| self.assertIn("Traceback (most recent call last):", err) | |
| self.assertIn("ZeroDivisionError", err) | |
| self.assertNotIn("Unhandled exception", err) | |
| def test_print_exception_stderr_is_none_2(self): | |
| script = r"""if True: | |
| import sys | |
| import threading | |
| import time | |
| running = False | |
| def run(): | |
| global running | |
| running = True | |
| while running: | |
| time.sleep(0.01) | |
| 1/0 | |
| sys.stderr = None | |
| t = threading.Thread(target=run) | |
| t.start() | |
| while not running: | |
| time.sleep(0.01) | |
| running = False | |
| t.join() | |
| """ | |
| rc, out, err = assert_python_ok("-c", script) | |
| self.assertEqual(out, b'') | |
| self.assertNotIn("Unhandled exception", err.decode()) | |
| def test_print_exception_gh_102056(self): | |
| # This used to crash. See gh-102056. | |
| script = r"""if True: | |
| import time | |
| import threading | |
| import _thread | |
| def f(): | |
| try: | |
| f() | |
| except RecursionError: | |
| f() | |
| def g(): | |
| try: | |
| raise ValueError() | |
| except* ValueError: | |
| f() | |
| def h(): | |
| time.sleep(1) | |
| _thread.interrupt_main() | |
| t = threading.Thread(target=h) | |
| t.start() | |
| g() | |
| t.join() | |
| """ | |
| assert_python_failure("-c", script) | |
| def test_bare_raise_in_brand_new_thread(self): | |
| def bare_raise(): | |
| raise | |
| class Issue27558(threading.Thread): | |
| exc = None | |
| def run(self): | |
| try: | |
| bare_raise() | |
| except Exception as exc: | |
| self.exc = exc | |
| thread = Issue27558() | |
| thread.start() | |
| thread.join() | |
| self.assertIsNotNone(thread.exc) | |
| self.assertIsInstance(thread.exc, RuntimeError) | |
| # explicitly break the reference cycle to not leak a dangling thread | |
| thread.exc = None | |
| def test_multithread_modify_file_noerror(self): | |
| # See issue25872 | |
| def modify_file(): | |
| with open(os_helper.TESTFN, 'w', encoding='utf-8') as fp: | |
| fp.write(' ') | |
| traceback.format_stack() | |
| self.addCleanup(os_helper.unlink, os_helper.TESTFN) | |
| threads = [ | |
| threading.Thread(target=modify_file) | |
| for i in range(100) | |
| ] | |
| for t in threads: | |
| t.start() | |
| t.join() | |
| def test_dummy_thread_on_interpreter_shutdown(self): | |
| # GH-130522: When `threading` held a reference to itself and then a | |
| # _DummyThread() object was created, destruction of the dummy thread | |
| # would emit an unraisable exception at shutdown, due to a lock being | |
| # destroyed. | |
| code = """if True: | |
| import sys | |
| import threading | |
| threading.x = sys.modules[__name__] | |
| x = threading._DummyThread() | |
| """ | |
| rc, out, err = assert_python_ok("-c", code) | |
| self.assertEqual(rc, 0) | |
| self.assertEqual(out, b"") | |
| self.assertEqual(err, b"") | |
| @requires_subprocess() | |
| @unittest.skipIf(os.name == 'nt', "signals don't work well on windows") | |
| def test_keyboard_interrupt_during_threading_shutdown(self): | |
| import subprocess | |
| source = f""" | |
| from threading import Thread | |
| import time | |
| import os | |
| def test(): | |
| print('a', flush=True, end='') | |
| time.sleep(10) | |
| for _ in range(3): | |
| Thread(target=test).start() | |
| """ | |
| with spawn_python("-c", source, stderr=subprocess.PIPE) as proc: | |
| self.assertEqual(proc.stdout.read(3), b'aaa') | |
| proc.send_signal(signal.SIGINT) | |
| proc.stderr.flush() | |
| error = proc.stderr.read() | |
| self.assertIn(b"KeyboardInterrupt", error) | |
| class ThreadRunFail(threading.Thread): | |
| def run(self): | |
| raise ValueError("run failed") | |
| class ExceptHookTests(BaseTestCase): | |
| def setUp(self): | |
| restore_default_excepthook(self) | |
| super().setUp() | |
| @force_not_colorized | |
| def test_excepthook(self): | |
| with support.captured_output("stderr") as stderr: | |
| thread = ThreadRunFail(name="excepthook thread") | |
| thread.start() | |
| thread.join() | |
| stderr = stderr.getvalue().strip() | |
| self.assertIn(f'Exception in thread {thread.name}:\n', stderr) | |
| self.assertIn('Traceback (most recent call last):\n', stderr) | |
| self.assertIn(' raise ValueError("run failed")', stderr) | |
| self.assertIn('ValueError: run failed', stderr) | |
| @support.cpython_only | |
| @force_not_colorized | |
| def test_excepthook_thread_None(self): | |
| # threading.excepthook called with thread=None: log the thread | |
| # identifier in this case. | |
| with support.captured_output("stderr") as stderr: | |
| try: | |
| raise ValueError("bug") | |
| except Exception as exc: | |
| args = threading.ExceptHookArgs([*sys.exc_info(), None]) | |
| try: | |
| threading.excepthook(args) | |
| finally: | |
| # Explicitly break a reference cycle | |
| args = None | |
| stderr = stderr.getvalue().strip() | |
| self.assertIn(f'Exception in thread {threading.get_ident()}:\n', stderr) | |
| self.assertIn('Traceback (most recent call last):\n', stderr) | |
| self.assertIn(' raise ValueError("bug")', stderr) | |
| self.assertIn('ValueError: bug', stderr) | |
| def test_system_exit(self): | |
| class ThreadExit(threading.Thread): | |
| def run(self): | |
| sys.exit(1) | |
| # threading.excepthook() silently ignores SystemExit | |
| with support.captured_output("stderr") as stderr: | |
| thread = ThreadExit() | |
| thread.start() | |
| thread.join() | |
| self.assertEqual(stderr.getvalue(), '') | |
| def test_custom_excepthook(self): | |
| args = None | |
| def hook(hook_args): | |
| nonlocal args | |
| args = hook_args | |
| try: | |
| with support.swap_attr(threading, 'excepthook', hook): | |
| thread = ThreadRunFail() | |
| thread.start() | |
| thread.join() | |
| self.assertEqual(args.exc_type, ValueError) | |
| self.assertEqual(str(args.exc_value), 'run failed') | |
| self.assertEqual(args.exc_traceback, args.exc_value.__traceback__) | |
| self.assertIs(args.thread, thread) | |
| finally: | |
| # Break reference cycle | |
| args = None | |
| def test_custom_excepthook_fail(self): | |
| def threading_hook(args): | |
| raise ValueError("threading_hook failed") | |
| err_str = None | |
| def sys_hook(exc_type, exc_value, exc_traceback): | |
| nonlocal err_str | |
| err_str = str(exc_value) | |
| with support.swap_attr(threading, 'excepthook', threading_hook), \ | |
| support.swap_attr(sys, 'excepthook', sys_hook), \ | |
| support.captured_output('stderr') as stderr: | |
| thread = ThreadRunFail() | |
| thread.start() | |
| thread.join() | |
| self.assertEqual(stderr.getvalue(), | |
| 'Exception in threading.excepthook:\n') | |
| self.assertEqual(err_str, 'threading_hook failed') | |
| def test_original_excepthook(self): | |
| def run_thread(): | |
| with support.captured_output("stderr") as output: | |
| thread = ThreadRunFail(name="excepthook thread") | |
| thread.start() | |
| thread.join() | |
| return output.getvalue() | |
| def threading_hook(args): | |
| print("Running a thread failed", file=sys.stderr) | |
| default_output = run_thread() | |
| with support.swap_attr(threading, 'excepthook', threading_hook): | |
| custom_hook_output = run_thread() | |
| threading.excepthook = threading.__excepthook__ | |
| recovered_output = run_thread() | |
| self.assertEqual(default_output, recovered_output) | |
| self.assertNotEqual(default_output, custom_hook_output) | |
| self.assertEqual(custom_hook_output, "Running a thread failed\n") | |
| class TimerTests(BaseTestCase): | |
| def setUp(self): | |
| BaseTestCase.setUp(self) | |
| self.callback_args = [] | |
| self.callback_event = threading.Event() | |
| def test_init_immutable_default_args(self): | |
| # Issue 17435: constructor defaults were mutable objects, they could be | |
| # mutated via the object attributes and affect other Timer objects. | |
| timer1 = threading.Timer(0.01, self._callback_spy) | |
| timer1.start() | |
| self.callback_event.wait() | |
| timer1.args.append("blah") | |
| timer1.kwargs["foo"] = "bar" | |
| self.callback_event.clear() | |
| timer2 = threading.Timer(0.01, self._callback_spy) | |
| timer2.start() | |
| self.callback_event.wait() | |
| self.assertEqual(len(self.callback_args), 2) | |
| self.assertEqual(self.callback_args, [((), {}), ((), {})]) | |
| timer1.join() | |
| timer2.join() | |
| def _callback_spy(self, *args, **kwargs): | |
| self.callback_args.append((args[:], kwargs.copy())) | |
| self.callback_event.set() | |
| class LockTests(lock_tests.LockTests): | |
| locktype = staticmethod(threading.Lock) | |
| class PyRLockTests(lock_tests.RLockTests): | |
| locktype = staticmethod(threading._PyRLock) | |
| @unittest.skipIf(threading._CRLock is None, 'RLock not implemented in C') | |
| class CRLockTests(lock_tests.RLockTests): | |
| locktype = staticmethod(threading._CRLock) | |
| def test_signature(self): # gh-102029 | |
| with warnings.catch_warnings(record=True) as warnings_log: | |
| threading.RLock() | |
| self.assertEqual(warnings_log, []) | |
| arg_types = [ | |
| ((1,), {}), | |
| ((), {'a': 1}), | |
| ((1, 2), {'a': 1}), | |
| ] | |
| for args, kwargs in arg_types: | |
| with self.subTest(args=args, kwargs=kwargs): | |
| self.assertRaises(TypeError, threading.RLock, *args, **kwargs) | |
| # Subtypes with custom `__init__` are allowed (but, not recommended): | |
| class CustomRLock(self.locktype): | |
| def __init__(self, a, *, b) -> None: | |
| super().__init__() | |
| with warnings.catch_warnings(record=True) as warnings_log: | |
| CustomRLock(1, b=2) | |
| self.assertEqual(warnings_log, []) | |
| class EventTests(lock_tests.EventTests): | |
| eventtype = staticmethod(threading.Event) | |
| class ConditionAsRLockTests(lock_tests.RLockTests): | |
| # Condition uses an RLock by default and exports its API. | |
| locktype = staticmethod(threading.Condition) | |
| def test_constructor_noargs(self): | |
| self.skipTest("Condition allows positional arguments") | |
| def test_recursion_count(self): | |
| self.skipTest("Condition does not expose _recursion_count()") | |
| class ConditionTests(lock_tests.ConditionTests): | |
| condtype = staticmethod(threading.Condition) | |
| class SemaphoreTests(lock_tests.SemaphoreTests): | |
| semtype = staticmethod(threading.Semaphore) | |
| class BoundedSemaphoreTests(lock_tests.BoundedSemaphoreTests): | |
| semtype = staticmethod(threading.BoundedSemaphore) | |
| class BarrierTests(lock_tests.BarrierTests): | |
| barriertype = staticmethod(threading.Barrier) | |
| ## Test Synchronization tools for iterators ################ | |
| class ThreadingIteratorToolsTests(BaseTestCase): | |
| def test_serialize_serializes_concurrent_iteration(self): | |
| limit = 10_000 | |
| workers_count = 10 | |
| result = 0 | |
| result_lock = threading.Lock() | |
| start = threading.Event() | |
| def producer(limit): | |
| for x in range(limit): | |
| yield x | |
| def consumer(iterator): | |
| nonlocal result | |
| start.wait() | |
| total = 0 | |
| for x in iterator: | |
| total += x | |
| with result_lock: | |
| result += total | |
| iterator = threading.serialize_iterator(producer(limit)) | |
| workers = [ | |
| threading.Thread(target=consumer, args=(iterator,)) | |
| for _ in range(workers_count) | |
| ] | |
| with threading_helper.wait_threads_exit(): | |
| for worker in workers: | |
| worker.start() | |
| for worker in workers: | |
| # Wait for the worker thread to actually start. | |
| while worker.ident is None: | |
| time.sleep(0.1) | |
| start.set() | |
| for worker in workers: | |
| worker.join() | |
| self.assertEqual(result, limit * (limit - 1) // 2) | |
| def test_serialize_generator_methods(self): | |
| # A generator that yields and receives | |
| def echo(): | |
| try: | |
| while True: | |
| val = yield "ready" | |
| yield f"received {val}" | |
| except ValueError: | |
| yield "caught" | |
| it = threading.serialize_iterator(echo()) | |
| # Test __next__ | |
| self.assertEqual(next(it), "ready") | |
| # Test send() | |
| self.assertEqual(it.send("hello"), "received hello") | |
| self.assertEqual(next(it), "ready") | |
| # Test throw() | |
| self.assertEqual(it.throw(ValueError), "caught") | |
| # Test close() | |
| it.close() | |
| with self.assertRaises(StopIteration): | |
| next(it) | |
| def test_serialize_methods_attribute_error(self): | |
| # A standard iterator that does not have send/throw/close | |
| # should raise AttributeError when called. | |
| standard_it = threading.serialize_iterator([1, 2, 3]) | |
| with self.assertRaises(AttributeError): | |
| standard_it.send("foo") | |
| with self.assertRaises(AttributeError): | |
| standard_it.throw(ValueError) | |
| with self.assertRaises(AttributeError): | |
| standard_it.close() | |
| def test_serialize_generator_methods_locking(self): | |
| # Verifies that generator methods also acquire the lock. | |
| # We can test this by checking if the lock is held during the call. | |
| class LockCheckingGenerator: | |
| def __init__(self, lock): | |
| self.lock = lock | |
| def __iter__(self): | |
| return self | |
| def send(self, value): | |
| if not self.lock.locked(): | |
| raise RuntimeError("Lock not held during send()") | |
| return value | |
| def throw(self, *args): | |
| if not self.lock.locked(): | |
| raise RuntimeError("Lock not held during throw()") | |
| def close(self): | |
| if not self.lock.locked(): | |
| raise RuntimeError("Lock not held during close()") | |
| # Manually create the serialize object to inspect the lock | |
| it = threading.serialize_iterator([]) | |
| mock_gen = LockCheckingGenerator(it._lock) | |
| it._iterator = mock_gen | |
| # These should not raise RuntimeError | |
| it.send(1) | |
| it.throw(ValueError) | |
| it.close() | |
| def test_serialize_next_exception(self): | |
| # Verify exception pass through for calls to next() | |
| def f(): | |
| raise RuntimeError | |
| yield None | |
| g = threading.serialize_iterator(f()) | |
| with self.assertRaises(RuntimeError): | |
| next(g) | |
| def test_synchronized_serializes_generator_instances(self): | |
| unique = 10 | |
| repetitions = 5 | |
| limit = 100 | |
| start = threading.Event() | |
| @threading.synchronized_iterator | |
| def atomic_counter(): | |
| # The sleep widens the race window that would exist without | |
| # synchronization between yielding a value and advancing state. | |
| i = 0 | |
| while True: | |
| yield i | |
| time.sleep(0.0005) | |
| i += 1 | |
| def consumer(counter): | |
| start.wait() | |
| for _ in range(limit): | |
| next(counter) | |
| unique_counters = [atomic_counter() for _ in range(unique)] | |
| counters = unique_counters * repetitions | |
| workers = [ | |
| threading.Thread(target=consumer, args=(counter,)) | |
| for counter in counters | |
| ] | |
| with threading_helper.wait_threads_exit(): | |
| for worker in workers: | |
| worker.start() | |
| start.set() | |
| for worker in workers: | |
| worker.join() | |
| self.assertEqual( | |
| {next(counter) for counter in unique_counters}, | |
| {limit * repetitions}, | |
| ) | |
| def test_synchronized_preserves_wrapped_metadata(self): | |
| def gen(): | |
| yield 1 | |
| wrapped = threading.synchronized_iterator(gen) | |
| self.assertEqual(wrapped.__name__, gen.__name__) | |
| self.assertIs(wrapped.__wrapped__, gen) | |
| self.assertEqual(list(wrapped()), [1]) | |
| def test_concurrent_tee_supports_concurrent_consumers(self): | |
| limit = 5_000 | |
| num_threads = 25 | |
| successes = 0 | |
| failures = [] | |
| result_lock = threading.Lock() | |
| start = threading.Event() | |
| expected = list(range(limit)) | |
| def producer(limit): | |
| for x in range(limit): | |
| yield x | |
| def consumer(iterator): | |
| nonlocal successes | |
| start.wait() | |
| items = list(iterator) | |
| with result_lock: | |
| if items == expected: | |
| successes += 1 | |
| else: | |
| failures.append(items[:20]) | |
| tees = threading.concurrent_tee(producer(limit), n=num_threads) | |
| workers = [ | |
| threading.Thread(target=consumer, args=(iterator,)) | |
| for iterator in tees | |
| ] | |
| with threading_helper.wait_threads_exit(): | |
| for worker in workers: | |
| worker.start() | |
| start.set() | |
| for worker in workers: | |
| worker.join() | |
| self.assertEqual(failures, []) | |
| self.assertEqual(successes, len(tees)) | |
| # Verify that locks are shared | |
| self.assertEqual(len({id(t_obj.lock) for t_obj in tees}), 1) | |
| def test_concurrent_tee_zero_iterators(self): | |
| self.assertEqual(threading.concurrent_tee(range(10), n=0), ()) | |
| def test_concurrent_tee_negative_n(self): | |
| with self.assertRaises(ValueError): | |
| threading.concurrent_tee(range(10), n=-1) | |
| ################# | |
| class MiscTestCase(unittest.TestCase): | |
| def test__all__(self): | |
| restore_default_excepthook(self) | |
| extra = {"ThreadError"} | |
| not_exported = {'currentThread', 'activeCount'} | |
| support.check__all__(self, threading, ('threading', '_thread'), | |
| extra=extra, not_exported=not_exported) | |
| @unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name") | |
| @unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name") | |
| def test_set_name(self): | |
| # Ensure main thread name is restored after test | |
| self.addCleanup(_thread.set_name, _thread._get_name()) | |
| # set_name() limit in bytes | |
| truncate = getattr(_thread, "_NAME_MAXLEN", None) | |
| limit = truncate or 100 | |
| tests = [ | |
| # test short ASCII name | |
| "CustomName", | |
| # test short non-ASCII name | |
| "namé€", | |
| # embedded null character: name is truncated | |
| # at the first null character | |
| "embed\0null", | |
| # Test long ASCII names (not truncated) | |
| "x" * limit, | |
| # Test long ASCII names (truncated) | |
| "x" * (limit + 10), | |
| # Test long non-ASCII name (truncated) | |
| "x" * (limit - 1) + "é€", | |
| # Test long non-BMP names (truncated) creating surrogate pairs | |
| # on Windows | |
| "x" * (limit - 1) + "\U0010FFFF", | |
| "x" * (limit - 2) + "\U0010FFFF" * 2, | |
| "x" + "\U0001f40d" * limit, | |
| "xx" + "\U0001f40d" * limit, | |
| "xxx" + "\U0001f40d" * limit, | |
| "xxxx" + "\U0001f40d" * limit, | |
| ] | |
| if os_helper.FS_NONASCII: | |
| tests.append(f"nonascii:{os_helper.FS_NONASCII}") | |
| if os_helper.TESTFN_UNENCODABLE: | |
| tests.append(os_helper.TESTFN_UNENCODABLE) | |
| if sys.platform.startswith("sunos"): | |
| # Use ASCII encoding on Solaris/Illumos/OpenIndiana | |
| encoding = "ascii" | |
| else: | |
| encoding = sys.getfilesystemencoding() | |
| def work(): | |
| nonlocal work_name | |
| work_name = _thread._get_name() | |
| for name in tests: | |
| if not support.MS_WINDOWS: | |
| encoded = name.encode(encoding, "replace") | |
| if b'\0' in encoded: | |
| encoded = encoded.split(b'\0', 1)[0] | |
| if truncate is not None: | |
| encoded = encoded[:truncate] | |
| if sys.platform.startswith("sunos"): | |
| expected = encoded.decode("ascii", "surrogateescape") | |
| else: | |
| expected = os.fsdecode(encoded) | |
| else: | |
| size = 0 | |
| chars = [] | |
| for ch in name: | |
| if ord(ch) > 0xFFFF: | |
| size += 2 | |
| else: | |
| size += 1 | |
| if size > truncate: | |
| break | |
| chars.append(ch) | |
| expected = ''.join(chars) | |
| if '\0' in expected: | |
| expected = expected.split('\0', 1)[0] | |
| with self.subTest(name=name, expected=expected, thread="main"): | |
| _thread.set_name(name) | |
| self.assertEqual(_thread._get_name(), expected) | |
| with self.subTest(name=name, expected=expected, thread="worker"): | |
| work_name = None | |
| thread = threading.Thread(target=work, name=name) | |
| thread.start() | |
| thread.join() | |
| self.assertEqual(work_name, expected, | |
| f"{len(work_name)=} and {len(expected)=}") | |
| @unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name") | |
| @unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name") | |
| def test_change_name(self): | |
| # Change the name of a thread while the thread is running | |
| name1 = None | |
| name2 = None | |
| def work(): | |
| nonlocal name1, name2 | |
| name1 = _thread._get_name() | |
| threading.current_thread().name = "new name" | |
| name2 = _thread._get_name() | |
| thread = threading.Thread(target=work, name="name") | |
| thread.start() | |
| thread.join() | |
| self.assertEqual(name1, "name") | |
| self.assertEqual(name2, "new name") | |
| class InterruptMainTests(unittest.TestCase): | |
| def check_interrupt_main_with_signal_handler(self, signum): | |
| def handler(signum, frame): | |
| 1/0 | |
| old_handler = signal.signal(signum, handler) | |
| self.addCleanup(signal.signal, signum, old_handler) | |
| with self.assertRaises(ZeroDivisionError): | |
| _thread.interrupt_main() | |
| def check_interrupt_main_noerror(self, signum): | |
| handler = signal.getsignal(signum) | |
| try: | |
| # No exception should arise. | |
| signal.signal(signum, signal.SIG_IGN) | |
| _thread.interrupt_main(signum) | |
| signal.signal(signum, signal.SIG_DFL) | |
| _thread.interrupt_main(signum) | |
| finally: | |
| # Restore original handler | |
| signal.signal(signum, handler) | |
| @requires_gil_enabled("gh-118433: Flaky due to a longstanding bug") | |
| def test_interrupt_main_subthread(self): | |
| # Calling start_new_thread with a function that executes interrupt_main | |
| # should raise KeyboardInterrupt upon completion. | |
| def call_interrupt(): | |
| _thread.interrupt_main() | |
| t = threading.Thread(target=call_interrupt) | |
| with self.assertRaises(KeyboardInterrupt): | |
| t.start() | |
| t.join() | |
| t.join() | |
| def test_interrupt_main_mainthread(self): | |
| # Make sure that if interrupt_main is called in main thread that | |
| # KeyboardInterrupt is raised instantly. | |
| with self.assertRaises(KeyboardInterrupt): | |
| _thread.interrupt_main() | |
| def test_interrupt_main_with_signal_handler(self): | |
| self.check_interrupt_main_with_signal_handler(signal.SIGINT) | |
| self.check_interrupt_main_with_signal_handler(signal.SIGTERM) | |
| def test_interrupt_main_noerror(self): | |
| self.check_interrupt_main_noerror(signal.SIGINT) | |
| self.check_interrupt_main_noerror(signal.SIGTERM) | |
| def test_interrupt_main_invalid_signal(self): | |
| self.assertRaises(ValueError, _thread.interrupt_main, -1) | |
| self.assertRaises(ValueError, _thread.interrupt_main, signal.NSIG) | |
| self.assertRaises(ValueError, _thread.interrupt_main, 1000000) | |
| @threading_helper.reap_threads | |
| def test_can_interrupt_tight_loops(self): | |
| cont = [True] | |
| started = [False] | |
| interrupted = [False] | |
| def worker(started, cont, interrupted): | |
| iterations = 100_000_000 | |
| started[0] = True | |
| while cont[0]: | |
| if iterations: | |
| iterations -= 1 | |
| else: | |
| return | |
| pass | |
| interrupted[0] = True | |
| t = threading.Thread(target=worker,args=(started, cont, interrupted)) | |
| t.start() | |
| while not started[0]: | |
| pass | |
| cont[0] = False | |
| t.join() | |
| self.assertTrue(interrupted[0]) | |
| class AtexitTests(unittest.TestCase): | |
| def test_atexit_output(self): | |
| rc, out, err = assert_python_ok("-c", """if True: | |
| import threading | |
| def run_last(): | |
| print('parrot') | |
| threading._register_atexit(run_last) | |
| """) | |
| self.assertFalse(err) | |
| self.assertEqual(out.strip(), b'parrot') | |
| def test_atexit_called_once(self): | |
| rc, out, err = assert_python_ok("-c", """if True: | |
| import threading | |
| from unittest.mock import Mock | |
| mock = Mock() | |
| threading._register_atexit(mock) | |
| mock.assert_not_called() | |
| # force early shutdown to ensure it was called once | |
| threading._shutdown() | |
| mock.assert_called_once() | |
| """) | |
| self.assertFalse(err) | |
| @force_not_colorized | |
| def test_atexit_after_shutdown(self): | |
| # The only way to do this is by registering an atexit within | |
| # an atexit, which is intended to raise an exception. | |
| rc, out, err = assert_python_ok("-c", """if True: | |
| import threading | |
| def func(): | |
| pass | |
| def run_last(): | |
| threading._register_atexit(func) | |
| threading._register_atexit(run_last) | |
| """) | |
| self.assertTrue(err) | |
| self.assertIn("RuntimeError: can't register atexit after shutdown", | |
| err.decode()) | |
| if __name__ == "__main__": | |
| unittest.main() |