diff --git a/.gitignore b/.gitignore index 338a6437ca..b5887be53b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ Lib/site-packages/* Lib/test/data/* !Lib/test/data/README cpython/ - +.claude/scheduled_tasks.lock \ No newline at end of file diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index b6fbf146a9..0af8d7add3 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -530,10 +530,16 @@ pub(crate) mod _thread { // Increment thread count when thread actually starts executing vm.state.thread_count.fetch_add(1); - match func.invoke(args, vm) { - Ok(_obj) => {} - Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {} - Err(exc) => { + // Inner scope: drop `func` (and its Python refs) before the thread + // slot is torn down below. Otherwise the parameter `func` would drop + // at end-of-function, after cleanup_current_thread_frames has cleared + // CURRENT_THREAD_SLOT, and a weakref callback fired during that drop + // would panic in push_thread_frame. + { + let func = func; + if let Err(exc) = func.invoke(args, vm) + && !exc.fast_isinstance(vm.ctx.exceptions.system_exit) + { vm.run_unraisable( exc, Some("Exception ignored in thread started by".to_owned()), @@ -1663,11 +1669,18 @@ pub(crate) mod _thread { // Increment thread count when thread actually starts executing vm_state.thread_count.fetch_add(1); - // Run the function - match func.invoke((), vm) { - Ok(_) => {} - Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {} - Err(exc) => { + // Inner scope: drop `func` (and its Python refs) before the + // outer scopeguard::defer tears down the thread slot. As a + // `move` closure capture, `func` would otherwise drop after + // all locals (including the scopeguard `_guard`), and a + // weakref callback fired during that drop would panic in + // push_thread_frame. + { + let func = func; + // Run the function + if let Err(exc) = func.invoke((), vm) + && !exc.fast_isinstance(vm.ctx.exceptions.system_exit) + { vm.run_unraisable( exc, Some("Exception ignored in thread started by".to_owned()), diff --git a/extra_tests/snippets/stdlib_threading.py b/extra_tests/snippets/stdlib_threading.py index f35d7e9d08..cb989e1fd3 100644 --- a/extra_tests/snippets/stdlib_threading.py +++ b/extra_tests/snippets/stdlib_threading.py @@ -1,6 +1,7 @@ import multiprocessing import os import threading +import time def import_in_thread(module_name): @@ -62,6 +63,48 @@ def start_fork_process_after_thread(): assert process.exitcode == 0, process.exitcode +def thread_join_ordering(): + output = [] + + def thread_function(name): + output.append((name, 0)) + time.sleep(2.0) + output.append((name, 1)) + + output.append((0, 0)) + x = threading.Thread(target=thread_function, args=(1,)) + output.append((0, 1)) + x.start() + output.append((0, 2)) + x.join() + output.append((0, 3)) + + assert len(output) == 6, output + # CPython has [(1, 0), (0, 2)] for the middle 2, but we have [(0, 2), (1, 0)] + # TODO: maybe fix this, if it turns out to be a problem? + # assert output == [(0, 0), (0, 1), (1, 0), (0, 2), (1, 1), (0, 3)] + + +def thread_exit_without_join(): + # Regression for https://github.com/RustPython/RustPython/issues/7813: + # a thread started without ``.join()`` must exit cleanly even when the + # captured target callable drops during teardown (which can fire + # weakref callbacks that re-enter the VM). + output = [] + + def runner(): + output.append("runner done") + + threading.Thread(target=runner).start() + time.sleep(1) + output.append("main done") + assert "runner done" in output, output + assert "main done" in output, output + + +thread_join_ordering() +thread_exit_without_join() + import_in_thread("functools") import_in_thread("tempfile") import_in_thread("multiprocessing.connection") diff --git a/extra_tests/snippets/test_threading.py b/extra_tests/snippets/test_threading.py deleted file mode 100644 index 4d7c29f509..0000000000 --- a/extra_tests/snippets/test_threading.py +++ /dev/null @@ -1,24 +0,0 @@ -import threading -import time - -output = [] - - -def thread_function(name): - output.append((name, 0)) - time.sleep(2.0) - output.append((name, 1)) - - -output.append((0, 0)) -x = threading.Thread(target=thread_function, args=(1,)) -output.append((0, 1)) -x.start() -output.append((0, 2)) -x.join() -output.append((0, 3)) - -assert len(output) == 6, output -# CPython has [(1, 0), (0, 2)] for the middle 2, but we have [(0, 2), (1, 0)] -# TODO: maybe fix this, if it turns out to be a problem? -# assert output == [(0, 0), (0, 1), (1, 0), (0, 2), (1, 1), (0, 3)]