Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ Lib/site-packages/*
Lib/test/data/*
!Lib/test/data/README
cpython/

.claude/scheduled_tasks.lock
31 changes: 22 additions & 9 deletions crates/vm/src/stdlib/_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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()),
Expand Down
43 changes: 43 additions & 0 deletions extra_tests/snippets/stdlib_threading.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import multiprocessing
import os
import threading
import time


def import_in_thread(module_name):
Expand Down Expand Up @@ -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")
Expand Down
24 changes: 0 additions & 24 deletions extra_tests/snippets/test_threading.py

This file was deleted.

Loading