mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Preserve Unicode in thread unraisable messages Keep the callable representation as WTF-8 for unraisable hooks and audit callbacks. Preserve the existing String entry point and stderr fallback; this does not claim lossless stderr rendering. Assisted-by: OpenAI Codex:model unavailable * Use the WTF-8 string conversion constructor Use the existing From<&str> implementation to copy the message prefix without changing Unicode semantics. Assisted-by: OpenAI Codex:model unavailable * Preserve cross-interpreter marshal error causes Wrap failures from existing code/function serialization helpers as NotShareableError, matching the CPython 3.15 marshal boundary while retaining the original exception cause. No new test C API or lifecycle behavior. Assisted-by: OpenAI Codex:model-unavailable * Enforce syslog interpreter boundaries and sync queue cleanup Reject subinterpreter openlog/closelog before audit or host mutation, and reject implicit syslog opening after audit when the process logger is not already open, matching CPython 3.15 guard placement and error text. Already-open logging and setlogmask remain unchanged. Migrate concurrent/interpreters/_queues.py through scripts/update_lib to CPython d220409a13b77de7a6c30abe16ea90db01528edc, removing unused eager pickle import and exception targets. Native serialization imports pickle on demand; module import timing and debug locals may nevertheless differ. No new fixtures or interpreter lifecycle mechanisms are introduced. Assisted-by: OpenAI Codex:model version unavailable
2480 lines
92 KiB
Rust
2480 lines
92 KiB
Rust
//! Implementation of the _thread module
|
|
|
|
#[cfg(all(unix, feature = "threading", feature = "host_env"))]
|
|
pub(crate) use _thread::after_fork_child;
|
|
|
|
pub use _thread::get_ident;
|
|
|
|
#[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
|
|
pub(crate) use _thread::{
|
|
CurrentFrameSlot, HandleEntry, RawRMutex, ShutdownEntry, get_all_current_frames,
|
|
init_main_thread_ident, module_def,
|
|
};
|
|
|
|
#[pymodule]
|
|
pub(crate) mod _thread {
|
|
use parking_lot::{
|
|
RawMutex, RawThreadId,
|
|
lock_api::{RawMutex as RawMutexT, RawMutexTimed, RawReentrantMutex},
|
|
};
|
|
|
|
use crate::{
|
|
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
|
|
builtins::{
|
|
PyBaseExceptionRef, PyDictRef, PyIntRef, PyStr, PyStrRef, PyTupleRef, PyType, PyTypeRef,
|
|
},
|
|
common::{lock::PyMutex, wtf8::Wtf8Buf},
|
|
convert::ToPyException,
|
|
frame::FrameObjectRef,
|
|
function::{
|
|
ArgCallable, ArgIndex, ArgumentError, DefaultRepr, FromArgs, FuncArgs, KwArgs,
|
|
NameExcInfo, OptionalArg, Param, ParamKind, PosArgs, PySetterValue, TimeoutSeconds,
|
|
},
|
|
object::{Traverse, TraverseFn},
|
|
types::{Constructor, GetAttr, PyStructSequence, Representable, SetAttr},
|
|
};
|
|
|
|
use alloc::sync::{Arc, Weak};
|
|
use core::{cell::RefCell, fmt, time::Duration};
|
|
use std::thread;
|
|
|
|
use rustpython_common::str::levenshtein::{MOVE_COST, levenshtein_distance};
|
|
#[cfg(any(unix, windows))]
|
|
use rustpython_host_env::thread as host_thread;
|
|
|
|
#[cfg(all(not(target_arch = "wasm32"), feature = "host_env"))]
|
|
use crate::signal::SignalNum;
|
|
|
|
// PYTHREAD_NAME: show current thread name
|
|
pub(crate) const PYTHREAD_NAME: Option<&str> = cfg_select! {
|
|
windows => Some("nt"),
|
|
unix => Some("pthread"),
|
|
any(target_os = "solaris", target_os = "illumos") => Some("solaris"),
|
|
_ => None,
|
|
};
|
|
|
|
const TIMEOUT_MAX_IN_MICROSECONDS: i64 = if cfg!(target_os = "windows") {
|
|
0xffffffff * 1_000
|
|
} else {
|
|
i64::MAX / 1_000
|
|
};
|
|
|
|
/// [CPython `SYSTEM_PAGE_SIZE`](https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_obmalloc.h#L170)
|
|
const SYSTEM_PAGE_SIZE: usize = 4 * 1024;
|
|
|
|
// TODO: Check for sanitize once https://github.com/rust-lang/rust/issues/39699 is closed
|
|
/// [CPython `_PyOS_LOG2_STACK_MARGIN`](https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_pythonrun.h#L41-L47)
|
|
const PY_OS_LOG2_STACK_MARGIN: u32 = cfg_select! {
|
|
debug_assertions => 12,
|
|
_ => 11,
|
|
};
|
|
|
|
/// [CPython `_PyOS_STACK_MARGIN`](https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_pythonrun.h#L48)
|
|
const PY_OS_STACK_MARGIN: usize = 1 << PY_OS_LOG2_STACK_MARGIN;
|
|
|
|
/// [CPython `_PyOS_STACK_MARGIN_BYTES`](https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_pythonrun.h#L49)
|
|
const PY_OS_STACK_MARGIN_BYTES: usize = PY_OS_STACK_MARGIN * size_of::<*const ()>();
|
|
|
|
// TODO: Check for sanitize once https://github.com/rust-lang/rust/issues/39699 is closed
|
|
/// [CPython `_PyOS_MIN_STACK_SIZE`](https://github.com/python/cpython/blob/v3.14.5/Include/internal/pycore_pythonrun.h#L57-L61)
|
|
const PY_OS_MIN_STACK_SIZE: usize = PY_OS_STACK_MARGIN_BYTES * 3;
|
|
|
|
// this is a value in seconds
|
|
#[pyattr]
|
|
const TIMEOUT_MAX: f64 = (TIMEOUT_MAX_IN_MICROSECONDS / 1_000_000) as f64;
|
|
|
|
#[pyattr]
|
|
fn error(vm: &VirtualMachine) -> PyTypeRef {
|
|
vm.ctx.exceptions.runtime_error.to_owned()
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct AcquireArgs {
|
|
#[pyarg(any, default = true)]
|
|
blocking: bool,
|
|
// No timeout; blocks until the lock is acquired.
|
|
#[pyarg(any, default = TimeoutSeconds::new(-1.0), py_default = "-1")]
|
|
timeout: TimeoutSeconds,
|
|
}
|
|
|
|
fn default_acquire_args() -> AcquireArgs {
|
|
AcquireArgs {
|
|
blocking: true,
|
|
timeout: TimeoutSeconds::new(-1.0),
|
|
}
|
|
}
|
|
|
|
fn validate_acquire_args(args: AcquireArgs, vm: &VirtualMachine) -> PyResult<(bool, f64)> {
|
|
let timeout = args.timeout.to_secs_f64();
|
|
// CPython checks the signed nanosecond range before acquire-specific restrictions.
|
|
let timeout_ns = timeout * 1_000_000_000.0;
|
|
if !(i64::MIN as f64..-(i64::MIN as f64)).contains(&timeout_ns) {
|
|
return Err(vm.new_overflow_error("timestamp out of range for C PyTime_t"));
|
|
}
|
|
if !args.blocking && timeout != -1.0 {
|
|
return Err(vm.new_value_error("can't specify a timeout for a non-blocking call"));
|
|
}
|
|
if args.blocking && timeout < 0.0 && timeout != -1.0 {
|
|
return Err(vm.new_value_error("timeout value must be a non-negative number"));
|
|
}
|
|
if timeout > TIMEOUT_MAX {
|
|
return Err(vm.new_overflow_error("timeout value is too large"));
|
|
}
|
|
Ok((args.blocking, timeout))
|
|
}
|
|
|
|
macro_rules! acquire_lock_impl {
|
|
($mu:expr, $blocking:expr, $timeout:expr, $vm:expr) => {{ acquire_lock_impl!($mu, $blocking, $timeout, $vm, false) }};
|
|
($mu:expr, $blocking:expr, $timeout:expr, $vm:expr, $owned:expr) => {{
|
|
let (mu, blocking, timeout, vm) = ($mu, $blocking, $timeout, $vm);
|
|
let started = std::time::Instant::now();
|
|
// CPython rounds positive timeouts up to the next nanosecond.
|
|
let timeout = (timeout != -1.0)
|
|
.then(|| Duration::from_nanos((timeout * 1_000_000_000.0).ceil() as u64));
|
|
let mut remaining = timeout;
|
|
loop {
|
|
// Ownership gained in a signal handler does not make an
|
|
// already pending acquisition recursive.
|
|
let owned = $owned;
|
|
if !owned && mu.try_lock() {
|
|
break Ok(true);
|
|
}
|
|
if !blocking || remaining.is_some_and(|remaining| remaining.is_zero()) {
|
|
break Ok(false);
|
|
}
|
|
if vm
|
|
.state
|
|
.finalizing
|
|
.load(core::sync::atomic::Ordering::Acquire)
|
|
{
|
|
// Other Python threads cannot release the lock once
|
|
// finalization begins. Uncontested calls still work.
|
|
break Err(vm.new_python_finalization_error(
|
|
"cannot acquire lock at interpreter finalization",
|
|
));
|
|
}
|
|
let interval = Duration::from_millis(50);
|
|
let wait = match timeout {
|
|
Some(timeout) => match timeout.checked_sub(started.elapsed()) {
|
|
Some(remaining) if !remaining.is_zero() => remaining.min(interval),
|
|
_ => break Ok(false),
|
|
},
|
|
None => interval,
|
|
};
|
|
// parking_lot waits are not interrupted by OS signals. Poll
|
|
// without restarting the deadline or acquiring a lock before
|
|
// running callbacks. A handler may itself retain an RLock.
|
|
let acquired = vm.allow_threads(|| {
|
|
if owned {
|
|
thread::sleep(wait);
|
|
false
|
|
} else {
|
|
mu.try_lock_for(wait)
|
|
}
|
|
});
|
|
if acquired {
|
|
break Ok(true);
|
|
}
|
|
if timeout.is_some_and(|timeout| started.elapsed() >= timeout) {
|
|
break Ok(false);
|
|
}
|
|
vm.check_signals()?;
|
|
remaining = timeout.map(|timeout| timeout.saturating_sub(started.elapsed()));
|
|
}
|
|
}};
|
|
}
|
|
|
|
macro_rules! repr_lock_impl {
|
|
($zelf:expr) => {{
|
|
let status = if $zelf.mu.is_locked() {
|
|
"locked"
|
|
} else {
|
|
"unlocked"
|
|
};
|
|
Ok(format!(
|
|
"<{} {} object at {:#x}>",
|
|
status,
|
|
$zelf.class().name(),
|
|
$zelf.get_id()
|
|
))
|
|
}};
|
|
}
|
|
|
|
#[pyattr(name = "LockType")]
|
|
#[pyattr(name = "lock")]
|
|
#[pyclass(module = "_thread", name = "lock")]
|
|
#[derive(PyPayload)]
|
|
struct Lock {
|
|
mu: RawMutex,
|
|
}
|
|
|
|
impl fmt::Debug for Lock {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.pad("Lock")
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(Constructor, Representable), flags(HAS_WEAKREF))]
|
|
impl Py<Lock> {
|
|
#[pymethod]
|
|
#[pymethod(name = "acquire_lock")]
|
|
fn acquire(&self, args: AcquireArgs, vm: &VirtualMachine) -> PyResult<bool> {
|
|
let (blocking, timeout) = validate_acquire_args(args, vm)?;
|
|
acquire_lock_impl!(&self.mu, blocking, timeout, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __enter__(&self, vm: &VirtualMachine) -> PyResult<bool> {
|
|
self.acquire(default_acquire_args(), vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
#[pymethod(name = "release_lock")]
|
|
fn release(&self, vm: &VirtualMachine) -> PyResult<()> {
|
|
if !self.mu.is_locked() {
|
|
return Err(vm.new_runtime_error("release unlocked lock"));
|
|
}
|
|
unsafe { self.mu.unlock() };
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[pymethod]
|
|
fn _at_fork_reinit(&self, _vm: &VirtualMachine) {
|
|
// Overwrite lock state to unlocked. Do NOT call unlock() here —
|
|
// after fork(), unlock_slow() would try to unpark stale waiters.
|
|
unsafe { rustpython_common::lock::zero_reinit_after_fork(&self.mu) };
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __exit__(
|
|
&self,
|
|
_args: PosArgs<crate::PyObjectRef, NameExcInfo>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
self.release(vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn locked(&self) -> bool {
|
|
self.mu.is_locked()
|
|
}
|
|
}
|
|
|
|
impl Constructor for Lock {
|
|
type Args = ();
|
|
|
|
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
|
|
Ok(Self { mu: RawMutex::INIT })
|
|
}
|
|
}
|
|
|
|
impl Representable for Lock {
|
|
#[inline]
|
|
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
|
|
repr_lock_impl!(zelf)
|
|
}
|
|
}
|
|
|
|
pub(crate) type RawRMutex = RawReentrantMutex<RawMutex, RawThreadId>;
|
|
#[pyattr]
|
|
#[pyclass(module = "_thread", name = "RLock")]
|
|
#[derive(PyPayload)]
|
|
struct RLock {
|
|
mu: RawRMutex,
|
|
count: core::sync::atomic::AtomicUsize,
|
|
}
|
|
|
|
impl fmt::Debug for RLock {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.pad("RLock")
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(Representable), flags(BASETYPE, HAS_WEAKREF))]
|
|
impl Py<RLock> {
|
|
#[pyslot]
|
|
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
let rlock_type = RLock::class(&vm.ctx);
|
|
let cls_init = cls.slots.init.load().map(crate::types::fn_addr);
|
|
let rlock_init = rlock_type.slots.init.load().map(crate::types::fn_addr);
|
|
if cls.is(rlock_type) || cls_init == rlock_init {
|
|
if !args.args.is_empty() {
|
|
return Err(vm.new_type_error("RLock() takes no positional arguments"));
|
|
}
|
|
if !args.kwargs.is_empty() {
|
|
return Err(vm.new_type_error("RLock() takes no keyword arguments"));
|
|
}
|
|
}
|
|
RLock {
|
|
mu: RawRMutex::INIT,
|
|
count: core::sync::atomic::AtomicUsize::new(0),
|
|
}
|
|
.into_ref_with_type(vm, cls)
|
|
.map(Into::into)
|
|
}
|
|
|
|
#[pymethod]
|
|
#[pymethod(name = "acquire_lock")]
|
|
fn acquire(&self, args: AcquireArgs, vm: &VirtualMachine) -> PyResult<bool> {
|
|
let (blocking, timeout) = validate_acquire_args(args, vm)?;
|
|
if self.mu.is_owned_by_current_thread() {
|
|
// Re-entrant acquisition: just increment our count.
|
|
// parking_lot stays at 1 level; we track recursion ourselves.
|
|
self.count
|
|
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
|
|
return Ok(true);
|
|
}
|
|
let result = acquire_lock_impl!(
|
|
&self.mu,
|
|
blocking,
|
|
timeout,
|
|
vm,
|
|
self.mu.is_owned_by_current_thread()
|
|
)?;
|
|
if result {
|
|
self.count.store(1, core::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __enter__(&self, vm: &VirtualMachine) -> PyResult<bool> {
|
|
self.acquire(default_acquire_args(), vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
#[pymethod(name = "release_lock")]
|
|
fn release(&self, vm: &VirtualMachine) -> PyResult<()> {
|
|
if !self.mu.is_owned_by_current_thread() {
|
|
return Err(vm.new_runtime_error("cannot release un-acquired lock"));
|
|
}
|
|
let prev = self
|
|
.count
|
|
.fetch_sub(1, core::sync::atomic::Ordering::Relaxed);
|
|
debug_assert!(prev > 0, "RLock count underflow");
|
|
if prev == 1 {
|
|
unsafe { self.mu.unlock() };
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[pymethod]
|
|
fn _at_fork_reinit(&self, _vm: &VirtualMachine) {
|
|
// Overwrite lock state to unlocked. Do NOT call unlock() here —
|
|
// after fork(), unlock_slow() would try to unpark stale waiters.
|
|
self.count.store(0, core::sync::atomic::Ordering::Relaxed);
|
|
unsafe { rustpython_common::lock::zero_reinit_after_fork(&self.mu) };
|
|
}
|
|
|
|
#[pymethod]
|
|
fn locked(&self) -> bool {
|
|
self.mu.is_locked()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _is_owned(&self) -> bool {
|
|
self.mu.is_owned_by_current_thread()
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _recursion_count(&self) -> usize {
|
|
if self.mu.is_owned_by_current_thread() {
|
|
self.count.load(core::sync::atomic::Ordering::Relaxed)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _release_save(&self, vm: &VirtualMachine) -> PyResult<(usize, u64)> {
|
|
if !self.mu.is_owned_by_current_thread() {
|
|
return Err(vm.new_runtime_error("cannot release un-acquired lock"));
|
|
}
|
|
let count = self.count.swap(0, core::sync::atomic::Ordering::Relaxed);
|
|
debug_assert!(count > 0, "RLock count underflow");
|
|
unsafe { self.mu.unlock() };
|
|
Ok((count, current_thread_id()))
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _acquire_restore(&self, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
let [count_obj, owner_obj] = state.as_slice() else {
|
|
return Err(
|
|
vm.new_type_error("_acquire_restore() argument 1 must be a 2-item tuple")
|
|
);
|
|
};
|
|
let count: usize = count_obj.clone().try_into_value(vm)?;
|
|
let _owner: u64 = owner_obj.clone().try_into_value(vm)?;
|
|
if count == 0 {
|
|
return Ok(());
|
|
}
|
|
vm.allow_threads(|| self.mu.lock());
|
|
self.count
|
|
.store(count, core::sync::atomic::Ordering::Relaxed);
|
|
Ok(())
|
|
}
|
|
|
|
#[pymethod]
|
|
fn __exit__(
|
|
&self,
|
|
_args: PosArgs<crate::PyObjectRef, NameExcInfo>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
self.release(vm)
|
|
}
|
|
}
|
|
|
|
impl Representable for RLock {
|
|
#[inline]
|
|
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
|
|
let count = zelf.count.load(core::sync::atomic::Ordering::Relaxed);
|
|
let status = if zelf.mu.is_locked() {
|
|
"locked"
|
|
} else {
|
|
"unlocked"
|
|
};
|
|
Ok(format!(
|
|
"<{} {} object count={} at {:#x}>",
|
|
status,
|
|
zelf.class().name(),
|
|
count,
|
|
zelf.get_id()
|
|
))
|
|
}
|
|
}
|
|
|
|
// uses pthread_self() on Unix for fork compatibility
|
|
#[pyfunction]
|
|
#[must_use]
|
|
pub fn get_ident() -> u64 {
|
|
current_thread_id()
|
|
}
|
|
|
|
#[cfg(all(
|
|
feature = "host_env",
|
|
any(
|
|
windows,
|
|
target_os = "linux",
|
|
target_os = "android",
|
|
target_os = "macos",
|
|
target_os = "ios",
|
|
target_os = "freebsd",
|
|
target_os = "openbsd",
|
|
target_os = "netbsd",
|
|
)
|
|
))]
|
|
#[pyfunction]
|
|
fn get_native_id() -> u64 {
|
|
host_thread::native_id()
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "threading"))]
|
|
#[pyfunction]
|
|
fn _stop_the_world_stats(vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
|
let stats = vm.state.stop_the_world.stats_snapshot();
|
|
let d = vm.ctx.new_dict();
|
|
d.set_item("stop_calls", vm.ctx.new_int(stats.stop_calls).into(), vm)?;
|
|
d.set_item(
|
|
"last_wait_ns",
|
|
vm.ctx.new_int(stats.last_wait_ns).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item(
|
|
"total_wait_ns",
|
|
vm.ctx.new_int(stats.total_wait_ns).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item("max_wait_ns", vm.ctx.new_int(stats.max_wait_ns).into(), vm)?;
|
|
d.set_item("poll_loops", vm.ctx.new_int(stats.poll_loops).into(), vm)?;
|
|
d.set_item(
|
|
"attached_seen",
|
|
vm.ctx.new_int(stats.attached_seen).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item(
|
|
"forced_parks",
|
|
vm.ctx.new_int(stats.forced_parks).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item(
|
|
"suspend_notifications",
|
|
vm.ctx.new_int(stats.suspend_notifications).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item(
|
|
"attach_wait_yields",
|
|
vm.ctx.new_int(stats.attach_wait_yields).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item(
|
|
"suspend_wait_yields",
|
|
vm.ctx.new_int(stats.suspend_wait_yields).into(),
|
|
vm,
|
|
)?;
|
|
d.set_item(
|
|
"world_stopped",
|
|
vm.ctx.new_bool(stats.world_stopped).into(),
|
|
vm,
|
|
)?;
|
|
Ok(d)
|
|
}
|
|
|
|
#[cfg(all(unix, feature = "threading"))]
|
|
#[pyfunction]
|
|
fn _stop_the_world_reset_stats(vm: &VirtualMachine) {
|
|
vm.state.stop_the_world.reset_stats();
|
|
}
|
|
|
|
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
|
|
#[pyattr]
|
|
const _NAME_MAXLEN: usize = host_thread::NAME_MAXLEN;
|
|
|
|
/// Truncate a Windows thread name to `_NAME_MAXLEN` UTF-16 code units,
|
|
/// dropping a trailing non-BMP character that would not fit as a pair.
|
|
#[cfg(windows)]
|
|
fn truncate_thread_name_wide(name: &crate::common::wtf8::Wtf8) -> Vec<u16> {
|
|
let encoded: Vec<u16> = name.encode_wide().collect();
|
|
let mut units = Vec::new();
|
|
let mut i = 0;
|
|
while i < encoded.len() {
|
|
let unit = encoded[i];
|
|
if unit == 0 {
|
|
break;
|
|
}
|
|
let width = match encoded.get(i + 1) {
|
|
Some(&lo)
|
|
if (0xD800..=0xDBFF).contains(&unit) && (0xDC00..=0xDFFF).contains(&lo) =>
|
|
{
|
|
2
|
|
}
|
|
_ => 1,
|
|
};
|
|
if units.len() + width > host_thread::NAME_MAXLEN {
|
|
break;
|
|
}
|
|
units.extend_from_slice(&encoded[i..i + width]);
|
|
i += width;
|
|
}
|
|
units.push(0);
|
|
units
|
|
}
|
|
|
|
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
|
|
#[derive(FromArgs)]
|
|
struct SetNameArgs {
|
|
#[pyarg(any)]
|
|
name: PyStrRef,
|
|
}
|
|
|
|
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
|
|
#[pyfunction]
|
|
fn set_name(SetNameArgs { name }: SetNameArgs, vm: &VirtualMachine) -> PyResult<()> {
|
|
#[cfg(windows)]
|
|
{
|
|
let units = truncate_thread_name_wide(name.as_wtf8());
|
|
host_thread::set_current_thread_name_wide(&units).map_err(|e| e.to_pyexception(vm))?;
|
|
}
|
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
{
|
|
let os_name = vm.state.codec_registry.encode_text(
|
|
name,
|
|
"utf-8",
|
|
Some(vm.ctx.new_utf8_str("replace")),
|
|
vm,
|
|
)?;
|
|
host_thread::set_current_thread_name_bytes(os_name.as_bytes());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
|
|
#[pyfunction(name = "_get_name")]
|
|
fn get_name(vm: &VirtualMachine) -> PyResult {
|
|
#[cfg(windows)]
|
|
{
|
|
let units =
|
|
host_thread::current_thread_name_wide().map_err(|e| e.to_pyexception(vm))?;
|
|
Ok(vm.ctx.new_str(Wtf8Buf::from_wide(&units)).into())
|
|
}
|
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
{
|
|
let bytes =
|
|
host_thread::current_thread_name(host_thread::NAME_MAXLEN.saturating_add(1))
|
|
.map_err(|e| e.to_pyexception(vm))?;
|
|
let os = unsafe { std::ffi::OsString::from_encoded_bytes_unchecked(bytes) };
|
|
Ok(vm.fsdecode(os).into())
|
|
}
|
|
}
|
|
|
|
/// Get OS-level thread ID (pthread_self on Unix, GetCurrentThreadId on Windows)
|
|
/// This is important for fork compatibility - the ID must remain stable after fork
|
|
fn current_thread_id() -> u64 {
|
|
cfg_select! {
|
|
any(unix, windows) => host_thread::current_thread_id(),
|
|
_ => thread_to_rust_id(&thread::current()),
|
|
}
|
|
}
|
|
|
|
/// Convert Rust thread to ID (used when no native thread id exists)
|
|
#[cfg(not(any(unix, windows)))]
|
|
fn thread_to_rust_id(t: &thread::Thread) -> u64 {
|
|
use core::hash::{Hash, Hasher};
|
|
|
|
struct U64Hash {
|
|
v: Option<u64>,
|
|
}
|
|
|
|
impl Hasher for U64Hash {
|
|
fn write(&mut self, _: &[u8]) {
|
|
unreachable!()
|
|
}
|
|
|
|
fn write_u64(&mut self, i: u64) {
|
|
self.v = Some(i);
|
|
}
|
|
|
|
fn finish(&self) -> u64 {
|
|
self.v.expect("should have written a u64")
|
|
}
|
|
}
|
|
|
|
let mut h = U64Hash { v: None };
|
|
t.id().hash(&mut h);
|
|
h.finish()
|
|
}
|
|
|
|
/// Get thread ID for a given thread handle (used by start_new_thread)
|
|
fn thread_to_id(handle: &thread::JoinHandle<()>) -> u64 {
|
|
#[cfg(unix)]
|
|
#[allow(clippy::unnecessary_cast)]
|
|
{
|
|
// On Unix, use pthread ID from the handle
|
|
use std::os::unix::thread::JoinHandleExt;
|
|
handle.as_pthread_t() as _
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::io::AsRawHandle;
|
|
host_thread::thread_id_from_handle(handle.as_raw_handle())
|
|
}
|
|
#[cfg(not(any(unix, windows)))]
|
|
{
|
|
thread_to_rust_id(handle.thread())
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
const fn allocate_lock() -> Lock {
|
|
Lock { mu: RawMutex::INIT }
|
|
}
|
|
|
|
struct StartNewThreadArgs {
|
|
func_obj: PyObjectRef,
|
|
args_obj: PyObjectRef,
|
|
func: ArgCallable,
|
|
args: PyTupleRef,
|
|
kwargs: Option<PyDictRef>,
|
|
}
|
|
|
|
impl FromArgs for StartNewThreadArgs {
|
|
const PARAMS: Option<&'static [Param]> = Some(&[
|
|
Param::positional_only("function"),
|
|
Param::positional_only("args"),
|
|
Param {
|
|
name: "kwargs",
|
|
kind: ParamKind::PositionalOnly,
|
|
default: Some(DefaultRepr::Raw("{}")),
|
|
},
|
|
]);
|
|
|
|
fn from_args(vm: &VirtualMachine, f_args: &mut FuncArgs) -> Result<Self, ArgumentError> {
|
|
if !f_args.kwargs.is_empty() {
|
|
return Err(vm
|
|
.new_type_error("start_new_thread() takes no keyword arguments")
|
|
.into());
|
|
}
|
|
|
|
let given = f_args.args.len();
|
|
if !(2..=3).contains(&given) {
|
|
return Err(vm
|
|
.new_arity_type_error("start_new_thread", 2..=3, given)
|
|
.into());
|
|
}
|
|
|
|
let func_obj = f_args.take_positional().unwrap();
|
|
let args_obj = f_args.take_positional().unwrap();
|
|
let kwargs_obj = f_args.take_positional();
|
|
|
|
if func_obj.to_callable().is_none() {
|
|
return Err(vm.new_type_error("first arg must be callable").into());
|
|
}
|
|
|
|
if !args_obj.fast_isinstance(vm.ctx.types.tuple_type) {
|
|
return Err(vm.new_type_error("2nd arg must be a tuple").into());
|
|
}
|
|
|
|
if kwargs_obj
|
|
.as_ref()
|
|
.is_some_and(|obj| !obj.fast_isinstance(vm.ctx.types.dict_type))
|
|
{
|
|
return Err(vm
|
|
.new_type_error("optional 3rd arg must be a dictionary")
|
|
.into());
|
|
}
|
|
|
|
let func: ArgCallable = func_obj.clone().try_into_value(vm)?;
|
|
let args: PyTupleRef = args_obj.clone().try_into_value(vm)?;
|
|
let kwargs: Option<PyDictRef> =
|
|
kwargs_obj.map(|obj| obj.try_into_value(vm)).transpose()?;
|
|
Ok(Self {
|
|
func_obj,
|
|
args_obj,
|
|
func,
|
|
args,
|
|
kwargs,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn start_new_thread(parsed: StartNewThreadArgs, vm: &VirtualMachine) -> PyResult<u64> {
|
|
let StartNewThreadArgs {
|
|
func_obj,
|
|
args_obj,
|
|
func,
|
|
args,
|
|
kwargs,
|
|
} = parsed;
|
|
|
|
vm.audit("_thread.start_new_thread", || {
|
|
(
|
|
func_obj,
|
|
args_obj,
|
|
kwargs
|
|
.as_ref()
|
|
.map_or_else(|| vm.ctx.none(), |k| k.clone().into()),
|
|
)
|
|
})?;
|
|
|
|
if !vm.state.allow_threads() {
|
|
return Err(vm.new_runtime_error(
|
|
"thread is not supported for isolated subinterpreters".to_owned(),
|
|
));
|
|
}
|
|
if vm
|
|
.state
|
|
.finalizing
|
|
.load(core::sync::atomic::Ordering::Acquire)
|
|
{
|
|
return Err(vm.new_exception_msg(
|
|
vm.ctx.exceptions.python_finalization_error.to_owned(),
|
|
"can't create new thread at interpreter shutdown"
|
|
.to_owned()
|
|
.into(),
|
|
));
|
|
}
|
|
|
|
let args = FuncArgs::new(
|
|
args.as_slice().to_vec(),
|
|
kwargs
|
|
.map_or_else(
|
|
|| Ok(Default::default()),
|
|
|k| {
|
|
k.to_attributes(vm, |vm| Err(vm.new_type_error("keywords must be strings")))
|
|
},
|
|
)?
|
|
.into_iter()
|
|
.map(|(k, v)| (k.as_str().to_owned(), v))
|
|
.collect::<KwArgs>(),
|
|
);
|
|
let thread_builder = apply_thread_stack_size(thread::Builder::new(), vm);
|
|
thread_builder
|
|
.spawn(
|
|
vm.new_thread()
|
|
.make_spawn_func(move |vm| run_thread(func, args, vm)),
|
|
)
|
|
.map(|handle| thread_to_id(&handle))
|
|
.map_err(|_err| vm.new_runtime_error("can't start new thread"))
|
|
}
|
|
|
|
fn report_unraisable_thread_exception(
|
|
exc: PyBaseExceptionRef,
|
|
func: &ArgCallable,
|
|
vm: &VirtualMachine,
|
|
) {
|
|
let msg = func.as_ref().repr(vm).ok().map(|repr| {
|
|
let mut message = Wtf8Buf::from("Exception ignored in thread started by ");
|
|
message.push_wtf8(repr.as_wtf8());
|
|
vm.ctx.new_str(message)
|
|
});
|
|
vm.run_unraisable_with_message(exc, msg, vm.ctx.none());
|
|
}
|
|
|
|
fn run_thread(func: ArgCallable, args: FuncArgs, vm: &VirtualMachine) {
|
|
// Increment thread count when thread actually starts executing
|
|
vm.state.thread_count.fetch_add(1);
|
|
|
|
// 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)
|
|
{
|
|
report_unraisable_thread_exception(exc, &func, vm);
|
|
}
|
|
}
|
|
for lock in SENTINELS.take() {
|
|
if lock.mu.is_locked() {
|
|
unsafe { lock.mu.unlock() };
|
|
}
|
|
}
|
|
// Clean up thread-local storage while VM context is still active
|
|
// This ensures __del__ methods are called properly
|
|
cleanup_thread_local_data();
|
|
// Clean up frame tracking
|
|
crate::vm::thread::cleanup_current_thread_frames(vm);
|
|
vm.state.thread_count.fetch_sub(1);
|
|
}
|
|
|
|
/// Default stack size for Python threads in **debug builds only**, where
|
|
/// Rust stack frames are substantially larger than in release. Rust's
|
|
/// `std::thread::Builder` otherwise defaults to 2 MB, which is too small
|
|
/// for the call chains the Python stdlib runs on helper threads in debug
|
|
/// (e.g. the SSL test server, see #7941). Release builds keep the prior
|
|
/// behavior — leave the stack size unset and let Rust's std default apply
|
|
/// — to avoid oversized virtual stack mappings when many threads spawn.
|
|
#[cfg(debug_assertions)]
|
|
const DEFAULT_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024;
|
|
|
|
/// Configure a `thread::Builder` with the stack size to use for a new
|
|
/// Python thread. Release builds use the value set via
|
|
/// `threading.stack_size(N)` when the user has provided one (non-zero) and
|
|
/// otherwise leave the builder unmodified (Rust's std default applies).
|
|
///
|
|
/// Debug builds take [`DEFAULT_THREAD_STACK_SIZE`] as a floor rather than
|
|
/// only as a default: an unoptimized `ExecutingFrame::run` reserves around
|
|
/// eighty kilobytes of stack where an optimized one reserves under a
|
|
/// thousand, so a size that holds a Python call chain in release holds
|
|
/// three of its frames here — starting a thread at all needs six. The
|
|
/// value `threading.stack_size()` reports is untouched.
|
|
fn apply_thread_stack_size(
|
|
thread_builder: thread::Builder,
|
|
vm: &VirtualMachine,
|
|
) -> thread::Builder {
|
|
let configured = vm.state.stacksize.load();
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
thread_builder.stack_size(configured.max(DEFAULT_THREAD_STACK_SIZE))
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
if configured == 0 {
|
|
thread_builder
|
|
} else {
|
|
thread_builder.stack_size(configured)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clean up thread-local data for the current thread.
|
|
/// This triggers __del__ on objects stored in thread-local variables.
|
|
fn cleanup_thread_local_data() {
|
|
// Move all guards out before dropping them. A local dict's __del__ may
|
|
// re-enter thread-local access and borrow LOCAL_GUARDS again.
|
|
let guards = LOCAL_GUARDS.take();
|
|
drop(guards);
|
|
}
|
|
|
|
#[cfg(all(not(target_arch = "wasm32"), feature = "host_env"))]
|
|
#[derive(FromArgs)]
|
|
struct InterruptMainArgs {
|
|
// The signal enum's repr is not valid syntax in a text signature.
|
|
#[pyarg(positional, optional, py_default = "signal.SIGINT")]
|
|
signum: OptionalArg<SignalNum>,
|
|
}
|
|
|
|
#[cfg(all(not(target_arch = "wasm32"), feature = "host_env"))]
|
|
#[pyfunction]
|
|
fn interrupt_main(args: InterruptMainArgs) -> PyResult<()> {
|
|
let sig = args.signum.unwrap_or(SignalNum::SIGINT);
|
|
crate::signal::set_interrupt_ex(sig)
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn exit(vm: &VirtualMachine) -> PyResult {
|
|
Err(vm.new_system_exit(vec![].into()))
|
|
}
|
|
|
|
thread_local!(static SENTINELS: RefCell<Vec<PyRef<Lock>>> = const { RefCell::new(Vec::new()) });
|
|
|
|
#[pyfunction]
|
|
fn _set_sentinel(vm: &VirtualMachine) -> PyRef<Lock> {
|
|
let lock = Lock { mu: RawMutex::INIT }.into_ref(&vm.ctx);
|
|
SENTINELS.with_borrow_mut(|sentinels| sentinels.push(lock.clone()));
|
|
lock
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct StackSizeArgs {
|
|
#[pyarg(positional, default = 0)]
|
|
size: ArgIndex,
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn stack_size(args: StackSizeArgs, vm: &VirtualMachine) -> PyResult<usize> {
|
|
const MIN_SIZE: usize = PY_OS_MIN_STACK_SIZE + SYSTEM_PAGE_SIZE;
|
|
let StackSizeArgs { size } = args;
|
|
|
|
let Ok(size) = size.as_ref().try_to_primitive(vm) else {
|
|
return Err(vm.new_value_error(format!("size must be at least {MIN_SIZE} bytes")));
|
|
};
|
|
|
|
if size != 0 && size < MIN_SIZE {
|
|
return Err(vm.new_value_error(format!("size must be at least {MIN_SIZE} bytes")));
|
|
}
|
|
|
|
Ok(vm.state.stacksize.swap(size))
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn _count(vm: &VirtualMachine) -> usize {
|
|
vm.state.thread_count.load()
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn daemon_threads_allowed(vm: &VirtualMachine) -> bool {
|
|
vm.state.allow_daemon_threads()
|
|
}
|
|
|
|
// Registry for non-daemon threads that need to be joined at shutdown
|
|
pub(crate) type ShutdownEntry = (
|
|
Weak<parking_lot::Mutex<ThreadHandleInner>>,
|
|
Weak<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
);
|
|
|
|
#[pyfunction]
|
|
fn _shutdown(vm: &VirtualMachine) {
|
|
// Wait for all non-daemon threads to finish
|
|
let current_ident = get_ident();
|
|
|
|
loop {
|
|
// Find a thread that's not finished and not the current thread
|
|
let handle_to_join = {
|
|
let mut handles = vm.state.shutdown_handles.lock();
|
|
// Clean up finished entries
|
|
handles.retain(|(inner_weak, _): &ShutdownEntry| {
|
|
inner_weak.upgrade().is_some_and(|inner| {
|
|
let guard = inner.lock();
|
|
guard.state != ThreadHandleState::Done && guard.ident != current_ident
|
|
})
|
|
});
|
|
|
|
// Find first unfinished handle
|
|
handles
|
|
.iter()
|
|
.find_map(|(inner_weak, done_event_weak): &ShutdownEntry| {
|
|
let inner = inner_weak.upgrade()?;
|
|
let done_event = done_event_weak.upgrade()?;
|
|
let guard = inner.lock();
|
|
if guard.state != ThreadHandleState::Done && guard.ident != current_ident {
|
|
Some((inner.clone(), done_event))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
};
|
|
|
|
match handle_to_join {
|
|
Some((inner, done_event)) => {
|
|
if let Err(exc) = ThreadHandle::join_internal(&inner, &done_event, None, vm) {
|
|
vm.run_unraisable(
|
|
exc,
|
|
Some(
|
|
"Exception ignored while joining a thread in _thread._shutdown()"
|
|
.to_owned(),
|
|
),
|
|
vm.ctx.none(),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
None => break, // No more threads to wait on
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add a non-daemon thread handle to the shutdown registry
|
|
fn add_to_shutdown_handles(
|
|
vm: &VirtualMachine,
|
|
inner: &Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
|
done_event: &Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
) {
|
|
let mut handles = vm.state.shutdown_handles.lock();
|
|
handles.push((Arc::downgrade(inner), Arc::downgrade(done_event)));
|
|
}
|
|
|
|
fn remove_from_shutdown_handles(
|
|
vm: &VirtualMachine,
|
|
inner: &Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
|
done_event: &Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
) {
|
|
let mut handles = vm.state.shutdown_handles.lock();
|
|
handles.retain(|(inner_weak, done_event_weak): &ShutdownEntry| {
|
|
let Some(registered_inner) = inner_weak.upgrade() else {
|
|
return false;
|
|
};
|
|
let Some(registered_done_event) = done_event_weak.upgrade() else {
|
|
return false;
|
|
};
|
|
!(Arc::ptr_eq(®istered_inner, inner)
|
|
&& Arc::ptr_eq(®istered_done_event, done_event))
|
|
});
|
|
}
|
|
|
|
#[derive(FromArgs)]
|
|
struct MakeThreadHandleArgs {
|
|
// An `int` only; the range is checked separately so it stays an OverflowError.
|
|
#[pyarg(positional, error_msg = "ident must be an integer")]
|
|
ident: PyIntRef,
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn _make_thread_handle(
|
|
args: MakeThreadHandleArgs,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<PyRef<ThreadHandle>> {
|
|
let ident = args.ident.try_to_primitive::<u64>(vm)?;
|
|
let handle = ThreadHandle::new(vm);
|
|
{
|
|
let mut inner = handle.inner.lock();
|
|
inner.ident = ident;
|
|
inner.state = ThreadHandleState::Running;
|
|
}
|
|
Ok(handle.into_ref(&vm.ctx))
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn _get_main_thread_ident(vm: &VirtualMachine) -> u64 {
|
|
vm.state.main_thread_ident.load()
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn _is_main_interpreter(vm: &VirtualMachine) -> bool {
|
|
vm.state.is_main_interpreter()
|
|
}
|
|
|
|
/// Initialize the main thread ident. Should be called once at interpreter startup.
|
|
pub(crate) fn init_main_thread_ident(vm: &VirtualMachine) {
|
|
let ident = get_ident();
|
|
vm.state.main_thread_ident.store(ident);
|
|
}
|
|
|
|
// This allows threading.py to import _excepthook and _ExceptHookArgs from _thread
|
|
#[pystruct_sequence_data]
|
|
struct ExceptHookArgsData {
|
|
exc_type: crate::PyObjectRef,
|
|
exc_value: crate::PyObjectRef,
|
|
exc_traceback: crate::PyObjectRef,
|
|
thread: crate::PyObjectRef,
|
|
}
|
|
|
|
#[pyattr]
|
|
#[pystruct_sequence(
|
|
name = "_ExceptHookArgs",
|
|
module = "_thread",
|
|
data = "ExceptHookArgsData"
|
|
)]
|
|
struct PyExceptHookArgs;
|
|
|
|
#[pyclass(with(PyStructSequence))]
|
|
impl PyExceptHookArgs {
|
|
#[extend_class]
|
|
fn extend_pyclass(ctx: &crate::vm::Context, class: &'static Py<crate::builtins::PyType>) {
|
|
// (type, (sequence, dict)). Set before the trait installs its default.
|
|
const EXCEPT_HOOK_ARGS_REDUCE: crate::function::PyMethodDef =
|
|
crate::function::PyMethodDef::new_const(
|
|
"__reduce__",
|
|
|zelf: crate::PyRef<crate::builtins::PyTuple>,
|
|
vm: &VirtualMachine|
|
|
-> PyTupleRef {
|
|
vm.new_tuple((
|
|
zelf.class().to_owned(),
|
|
(
|
|
vm.ctx.new_tuple(zelf.as_slice().to_vec()),
|
|
vm.ctx.new_dict(),
|
|
),
|
|
))
|
|
},
|
|
crate::function::PyMethodFlags::METHOD,
|
|
crate::function::ItemDoc::static_text("__reduce__($self, /)\n--\n\n"),
|
|
);
|
|
class.set_attr(
|
|
ctx.intern_str("__reduce__"),
|
|
EXCEPT_HOOK_ARGS_REDUCE.to_proper_method(class, ctx),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn _excepthook(args: crate::PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
|
|
// Type check: args must be _ExceptHookArgs
|
|
let args = args.downcast::<PyExceptHookArgs>().map_err(|_| {
|
|
vm.new_type_error("_thread.excepthook argument type must be ExceptHookArgs")
|
|
})?;
|
|
let fields = args.0.as_slice();
|
|
let (Some(exc_type), Some(exc_value), Some(exc_traceback), Some(thread)) =
|
|
(fields.first(), fields.get(1), fields.get(2), fields.get(3))
|
|
else {
|
|
return Err(
|
|
vm.new_type_error("_thread.excepthook argument type must be ExceptHookArgs")
|
|
);
|
|
};
|
|
let exc_type = exc_type.clone();
|
|
let exc_value = exc_value.clone();
|
|
let exc_traceback = exc_traceback.clone();
|
|
let thread = thread.clone();
|
|
|
|
// Silently ignore SystemExit (identity check)
|
|
if exc_type.is(vm.ctx.exceptions.system_exit.as_ref()) {
|
|
return Ok(());
|
|
}
|
|
|
|
// Get stderr - fall back to thread._stderr if sys.stderr is None
|
|
let file = match vm.sys_module.get_attr("stderr", vm) {
|
|
Ok(stderr) if !vm.is_none(&stderr) => stderr,
|
|
_ => {
|
|
if vm.is_none(&thread) {
|
|
// do nothing if sys.stderr is None and thread is None
|
|
return Ok(());
|
|
}
|
|
let thread_stderr = thread.get_attr("_stderr", vm)?;
|
|
if vm.is_none(&thread_stderr) {
|
|
// do nothing if sys.stderr is None and sys.stderr was None
|
|
// when the thread was created
|
|
return Ok(());
|
|
}
|
|
thread_stderr
|
|
}
|
|
};
|
|
|
|
// Print "Exception in thread {thread.name}:"
|
|
let thread_name = if !vm.is_none(&thread) {
|
|
thread
|
|
.get_attr("name", vm)
|
|
.ok()
|
|
.and_then(|n| n.str(vm).ok())
|
|
.map(|s| s.as_wtf8().to_owned())
|
|
} else {
|
|
None
|
|
};
|
|
let name = thread_name.unwrap_or_else(|| Wtf8Buf::from(format!("{}", get_ident())));
|
|
|
|
let _ = vm.call_method(&file, "write", (format!("Exception in thread {name}:\n"),));
|
|
|
|
// Display the traceback
|
|
if let Ok(traceback_mod) = vm.import("traceback", 0)
|
|
&& let Ok(print_exc) = traceback_mod.get_attr("print_exception", vm)
|
|
{
|
|
use crate::function::KwArgs;
|
|
let kwargs: KwArgs = vec![("file".to_owned(), file.clone())]
|
|
.into_iter()
|
|
.collect();
|
|
let _ = print_exc.call_with_args(
|
|
crate::function::FuncArgs::new(vec![exc_type, exc_value, exc_traceback], kwargs),
|
|
vm,
|
|
);
|
|
}
|
|
|
|
// Flush file
|
|
let _ = vm.call_method(&file, "flush", ());
|
|
Ok(())
|
|
}
|
|
|
|
// Thread-local storage for cleanup guards
|
|
// When a thread terminates, the guard is dropped, which triggers cleanup
|
|
thread_local! {
|
|
static LOCAL_GUARDS: RefCell<Vec<LocalGuard>> = const { RefCell::new(Vec::new()) };
|
|
}
|
|
|
|
// Guard that removes thread-local data when dropped
|
|
struct LocalGuard {
|
|
local: Weak<LocalData>,
|
|
thread_id: u64,
|
|
}
|
|
|
|
impl Drop for LocalGuard {
|
|
fn drop(&mut self) {
|
|
if let Some(local_data) = self.local.upgrade() {
|
|
// Remove from map while holding the lock, but drop the value
|
|
// outside the lock to prevent deadlock if __del__ accesses _local
|
|
let removed = local_data.state.lock().dicts.remove(&self.thread_id);
|
|
drop(removed);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LocalState {
|
|
init_args: FuncArgs,
|
|
dicts: std::collections::HashMap<u64, PyDictRef>,
|
|
}
|
|
|
|
unsafe impl Traverse for LocalState {
|
|
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
|
|
self.init_args.traverse(tracer_fn);
|
|
#[allow(clippy::iter_over_hash_type)]
|
|
for dict in self.dicts.values() {
|
|
dict.traverse(tracer_fn);
|
|
}
|
|
}
|
|
|
|
fn clear(&mut self, out: &mut Vec<crate::PyObjectRef>) {
|
|
out.append(&mut self.init_args.args);
|
|
out.extend(self.init_args.kwargs.drain(..).map(|(_, value)| value));
|
|
out.extend(self.dicts.drain().map(|(_, dict)| dict.into()));
|
|
}
|
|
}
|
|
|
|
// Shared data structure for Local
|
|
struct LocalData {
|
|
state: PyMutex<LocalState>,
|
|
}
|
|
|
|
impl fmt::Debug for LocalData {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("LocalData").finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
#[pyattr]
|
|
#[pyclass(module = "_thread", name = "_local", traverse = "manual")]
|
|
#[derive(Debug, PyPayload)]
|
|
struct Local {
|
|
inner: Arc<LocalData>,
|
|
}
|
|
|
|
unsafe impl Traverse for Local {
|
|
fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
|
|
self.inner.state.traverse(tracer_fn);
|
|
}
|
|
|
|
fn clear(&mut self, out: &mut Vec<crate::PyObjectRef>) {
|
|
if let Some(mut state) = self.inner.state.try_lock() {
|
|
state.clear(out);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Constructor for Local {
|
|
type Args = ();
|
|
|
|
fn py_new(_cls: &Py<PyType>, _args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
|
|
Err(vm.new_type_error("use slot_new"))
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(Constructor, GetAttr, SetAttr), flags(BASETYPE))]
|
|
impl Local {
|
|
fn custom_init(cls: &Py<PyType>, vm: &VirtualMachine) -> Option<crate::types::InitFunc> {
|
|
let cls_init = cls.slots.init.load()?;
|
|
let object_init = vm
|
|
.ctx
|
|
.types
|
|
.object_type
|
|
.slots
|
|
.init
|
|
.load()
|
|
.map(|init| crate::types::fn_addr(init));
|
|
(Some(crate::types::fn_addr(cls_init)) != object_init).then_some(cls_init)
|
|
}
|
|
|
|
fn create_dict(&self, vm: &VirtualMachine) -> (PyDictRef, bool) {
|
|
let thread_id = current_thread_id();
|
|
|
|
// Fast path: check if dict exists under lock
|
|
let value = self.inner.state.lock().dicts.get(&thread_id).cloned();
|
|
if let Some(dict) = value {
|
|
return (dict, false);
|
|
}
|
|
|
|
// Slow path: allocate dict outside lock to reduce lock hold time
|
|
let new_dict = vm.ctx.new_dict();
|
|
|
|
// Insert with double-check to handle races
|
|
let mut state = self.inner.state.lock();
|
|
use std::collections::hash_map::Entry;
|
|
let (dict, need_guard) = match state.dicts.entry(thread_id) {
|
|
Entry::Occupied(e) => (e.get().clone(), false),
|
|
Entry::Vacant(e) => {
|
|
e.insert(new_dict.clone());
|
|
(new_dict, true)
|
|
}
|
|
};
|
|
drop(state); // Release lock before TLS access
|
|
|
|
// Register cleanup guard only if we inserted a new entry
|
|
if need_guard {
|
|
let guard = LocalGuard {
|
|
local: Arc::downgrade(&self.inner),
|
|
thread_id,
|
|
};
|
|
LOCAL_GUARDS.with(|guards| {
|
|
guards.borrow_mut().push(guard);
|
|
});
|
|
}
|
|
|
|
(dict, need_guard)
|
|
}
|
|
|
|
fn remove_current_dict(&self) {
|
|
let thread_id = current_thread_id();
|
|
let guard = LOCAL_GUARDS.with(|guards| {
|
|
let mut guards = guards.borrow_mut();
|
|
guards
|
|
.iter()
|
|
.rposition(|guard| {
|
|
guard.thread_id == thread_id
|
|
&& guard.local.as_ptr() == Arc::as_ptr(&self.inner)
|
|
})
|
|
.map(|position| guards.remove(position))
|
|
});
|
|
|
|
if let Some(guard) = guard {
|
|
drop(guard);
|
|
} else {
|
|
let removed = self.inner.state.lock().dicts.remove(&thread_id);
|
|
drop(removed);
|
|
}
|
|
}
|
|
|
|
fn l_dict(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
|
let (dict, created) = zelf.create_dict(vm);
|
|
if !created {
|
|
return Ok(dict);
|
|
}
|
|
|
|
let Some(init) = Self::custom_init(zelf.class(), vm) else {
|
|
return Ok(dict);
|
|
};
|
|
let init_args = zelf.inner.state.lock().init_args.clone();
|
|
if let Err(err) = init(zelf.as_object(), init_args, vm) {
|
|
zelf.remove_current_dict();
|
|
return Err(err);
|
|
}
|
|
|
|
Ok(dict)
|
|
}
|
|
|
|
#[pygetset(name = "__dict__")]
|
|
fn dict(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyDictRef> {
|
|
Self::l_dict(&zelf, vm)
|
|
}
|
|
|
|
#[pyslot]
|
|
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
if !args.is_empty() && Self::custom_init(&cls, vm).is_none() {
|
|
return Err(vm.new_type_error("Initialization arguments are not supported"));
|
|
}
|
|
|
|
let zelf = Self {
|
|
inner: Arc::new(LocalData {
|
|
state: PyMutex::new(LocalState {
|
|
init_args: args,
|
|
dicts: std::collections::HashMap::new(),
|
|
}),
|
|
}),
|
|
}
|
|
.into_ref_with_type(vm, cls)?;
|
|
|
|
// type.__call__ invokes __init__ after __new__. Create this thread's
|
|
// dict first so assignments made by __init__ cannot recursively
|
|
// initialize the same local object.
|
|
zelf.create_dict(vm);
|
|
Ok(zelf.into())
|
|
}
|
|
}
|
|
|
|
impl GetAttr for Local {
|
|
fn getattro(zelf: &Py<Self>, attr: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
|
|
let l_dict = Self::l_dict(zelf, vm)?;
|
|
if attr.as_bytes() == b"__dict__" {
|
|
Ok(l_dict.into())
|
|
} else {
|
|
zelf.as_object()
|
|
.generic_getattr_opt(attr, Some(l_dict), vm)?
|
|
.ok_or_else(|| {
|
|
vm.new_attribute_error(format!(
|
|
"{} has no attribute '{}'",
|
|
zelf.class().name(),
|
|
attr
|
|
))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SetAttr for Local {
|
|
fn setattro(
|
|
zelf: &Py<Self>,
|
|
attr: &Py<PyStr>,
|
|
value: PySetterValue,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
if attr.as_bytes() == b"__dict__" {
|
|
Err(vm.new_attribute_error(format!(
|
|
"{} attribute '__dict__' is read-only",
|
|
zelf.class().name()
|
|
)))
|
|
} else {
|
|
let dict = Self::l_dict(zelf, vm)?;
|
|
if let PySetterValue::Assign(value) = value {
|
|
dict.set_item(attr, value, vm)?;
|
|
} else {
|
|
dict.del_item(attr, vm)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Registry of all ThreadHandles for fork cleanup
|
|
// Stores weak references so handles can be garbage collected normally
|
|
pub(crate) type HandleEntry = (
|
|
Weak<parking_lot::Mutex<ThreadHandleInner>>,
|
|
Weak<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
);
|
|
|
|
// Re-export type from vm::thread for PyGlobalState
|
|
pub(crate) use crate::vm::thread::CurrentFrameSlot;
|
|
|
|
/// Get all threads' current (top) frames. Used by sys._current_frames().
|
|
pub(crate) fn get_all_current_frames(vm: &VirtualMachine) -> Vec<(u64, FrameObjectRef)> {
|
|
// unix: read each thread's published top frame under stop-the-world so
|
|
// the owning thread is parked at a safepoint and cannot pop or free the
|
|
// frame while we take a strong reference. Request stop-the-world before
|
|
// the registry lock to avoid deadlocking a thread parking mid-registry.
|
|
//
|
|
// For the current thread, use TLS CURRENT_FRAME directly because
|
|
// stack-allocated frames only update TLS (not top_frame).
|
|
#[cfg(unix)]
|
|
{
|
|
use core::sync::atomic::Ordering;
|
|
let current_ident = get_ident();
|
|
vm.state.stop_the_world.stop_the_world(&vm.state);
|
|
scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); }
|
|
let registry = vm.state.thread_frames.lock();
|
|
registry
|
|
.iter()
|
|
.filter_map(|(id, slot)| {
|
|
if *id == current_ident {
|
|
// Current thread: materialize from TLS chain
|
|
crate::frame::current_thread_frame_materialize(vm).map(|frame| (*id, frame))
|
|
} else {
|
|
// Other threads: try top_frame first (FrameObject),
|
|
// fall back to top_iframe (may be a stack-allocated frame).
|
|
let top = slot.top_frame.load(Ordering::Relaxed);
|
|
if let Some(p) = core::ptr::NonNull::new(top) {
|
|
// SAFETY: world stopped -> the owning thread is parked
|
|
// with this frame on its chain, so it is alive.
|
|
let py = unsafe { p.as_ref() };
|
|
Some((*id, py.to_owned()))
|
|
} else {
|
|
// Stack-allocated frame: materialize from top_iframe.
|
|
// SAFETY: world stopped -> owning thread is parked.
|
|
let iframe_ptr = slot.top_iframe.load(Ordering::Relaxed)
|
|
as *const crate::frame::InterpreterFrame;
|
|
if !iframe_ptr.is_null() {
|
|
let iframe = unsafe { &*iframe_ptr };
|
|
// SAFETY: world stopped -> owning thread parked.
|
|
let fo = unsafe { iframe.materialize_detached_chain(vm) };
|
|
Some((*id, fo))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
#[cfg(not(unix))]
|
|
{
|
|
use core::sync::atomic::Ordering;
|
|
let current_ident = get_ident();
|
|
vm.state.stop_the_world.stop_the_world(&vm.state);
|
|
scopeguard::defer! { vm.state.stop_the_world.start_the_world(&vm.state); }
|
|
let registry = vm.state.thread_frames.lock();
|
|
registry
|
|
.iter()
|
|
.filter_map(|(id, slot)| {
|
|
if *id == current_ident {
|
|
// Current thread: materialize from TLS chain
|
|
crate::frame::current_thread_frame_materialize(vm).map(|frame| (*id, frame))
|
|
} else {
|
|
// Other threads: use top_iframe to include
|
|
// stack-allocated frames. Materialize the entire
|
|
// chain and link retained_back so f_back works.
|
|
// SAFETY: world stopped -> owning thread is parked.
|
|
let iframe_ptr = slot.top_iframe.load(Ordering::Relaxed)
|
|
as *const crate::frame::InterpreterFrame;
|
|
if !iframe_ptr.is_null() {
|
|
let iframe = unsafe { &*iframe_ptr };
|
|
// SAFETY: world stopped -> owning thread parked.
|
|
let fo = unsafe { iframe.materialize_detached_chain(vm) };
|
|
Some((*id, fo))
|
|
} else {
|
|
// Fall back to frames stack for FrameObject-only path
|
|
let frames = slot.frames.lock();
|
|
frames
|
|
.last()
|
|
.map(|fp| (*id, unsafe { fp.as_ref() }.to_owned()))
|
|
}
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Called after fork() in child process to mark all other threads as done.
|
|
/// This prevents join() from hanging on threads that don't exist in the child.
|
|
///
|
|
/// Precondition: `reinit_locks_after_fork()` has already been called, so all
|
|
/// parking_lot-based locks in VmState are in unlocked state.
|
|
#[cfg(all(unix, feature = "threading", feature = "host_env"))]
|
|
pub(crate) fn after_fork_child(vm: &VirtualMachine) {
|
|
let current_ident = get_ident();
|
|
|
|
// Update main thread ident - after fork, the current thread becomes the main thread
|
|
vm.state.main_thread_ident.store(current_ident);
|
|
|
|
// Reinitialize frame slot for current thread.
|
|
// Locks are already reinit'd, so lock() is safe.
|
|
crate::vm::thread::reinit_frame_slot_after_fork(vm);
|
|
|
|
// Clean up thread handles. All VmState locks were reinit'd to unlocked,
|
|
// so lock() won't deadlock. Per-thread Arc<Mutex<ThreadHandleInner>>
|
|
// locks are also reinit'd below before use.
|
|
{
|
|
let mut handles = vm.state.thread_handles.lock();
|
|
handles.retain(|(inner_weak, done_event_weak): &HandleEntry| {
|
|
let Some(inner) = inner_weak.upgrade() else {
|
|
return false;
|
|
};
|
|
let Some(done_event) = done_event_weak.upgrade() else {
|
|
return false;
|
|
};
|
|
|
|
// SAFETY: only the forking thread survives in the child.
|
|
unsafe { reinit_handle_after_fork(&inner, &done_event, current_ident) };
|
|
|
|
true
|
|
});
|
|
}
|
|
|
|
// Every shutdown entry comes from ThreadHandle::new and is also in
|
|
// thread_handles, so its completion event was already reset above.
|
|
// Clean up shutdown_handles.
|
|
{
|
|
let mut handles = vm.state.shutdown_handles.lock();
|
|
handles.retain(|(inner_weak, done_event_weak): &ShutdownEntry| {
|
|
let Some(inner) = inner_weak.upgrade() else {
|
|
return false;
|
|
};
|
|
let Some(done_event) = done_event_weak.upgrade() else {
|
|
return false;
|
|
};
|
|
|
|
reinit_parking_lot_mutex(&inner);
|
|
let mut inner_guard = inner.lock();
|
|
|
|
if inner_guard.ident == current_ident {
|
|
return true;
|
|
}
|
|
if inner_guard.state == ThreadHandleState::NotStarted {
|
|
return true;
|
|
}
|
|
|
|
inner_guard.state = ThreadHandleState::Done;
|
|
drop(inner_guard);
|
|
|
|
let (lock, cvar) = &*done_event;
|
|
reinit_parking_lot_mutex(lock);
|
|
reinit_parking_lot_condvar(cvar);
|
|
*lock.lock() = true;
|
|
|
|
false
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Repair a copied handle before any child thread cleanup or join.
|
|
///
|
|
/// Unlike CPython's atomic completion event, ours includes a mutex that a
|
|
/// parent joiner can hold while detached. Even the surviving thread's
|
|
/// completion mutex and condvar must lose that dead owner's/waiter's state.
|
|
/// Its completion value remains unchanged until the thread really exits.
|
|
///
|
|
/// # Safety
|
|
/// Only call in the single-threaded post-fork child, with no live guards or
|
|
/// waiters on these locks.
|
|
#[cfg(all(unix, feature = "host_env"))]
|
|
unsafe fn reinit_handle_after_fork(
|
|
inner: &parking_lot::Mutex<ThreadHandleInner>,
|
|
done_event: &(parking_lot::Mutex<bool>, parking_lot::Condvar),
|
|
current_ident: u64,
|
|
) {
|
|
reinit_parking_lot_mutex(inner);
|
|
let (lock, cvar) = done_event;
|
|
reinit_parking_lot_mutex(lock);
|
|
reinit_parking_lot_condvar(cvar);
|
|
let mut inner_guard = inner.lock();
|
|
if inner_guard.ident == current_ident || inner_guard.state == ThreadHandleState::NotStarted
|
|
{
|
|
return;
|
|
}
|
|
|
|
inner_guard.state = ThreadHandleState::Done;
|
|
// The OS thread did not survive. Do not pthread_detach a copied handle.
|
|
if let Some(handle) = inner_guard.join_handle.take() {
|
|
core::mem::forget(handle);
|
|
}
|
|
drop(inner_guard);
|
|
// Do not notify waiters: none survived the fork.
|
|
*lock.lock() = true;
|
|
}
|
|
|
|
/// Take a thread handle's completion mutex, detaching first.
|
|
///
|
|
/// A joiner holds this mutex across its `allow_threads` wait, so it can
|
|
/// still hold it when stop-the-world stops it. An attached thread that
|
|
/// blocked on it would never reach a safepoint, so the stop could never
|
|
/// complete and the holder would never be resumed to release it.
|
|
fn lock_done<'a>(
|
|
lock: &'a parking_lot::Mutex<bool>,
|
|
vm: &VirtualMachine,
|
|
) -> parking_lot::MutexGuard<'a, bool> {
|
|
vm.allow_threads(|| lock.lock())
|
|
}
|
|
|
|
/// Reset a parking_lot::Mutex to unlocked state after fork.
|
|
#[cfg(all(unix, feature = "host_env"))]
|
|
fn reinit_parking_lot_mutex<T: ?Sized>(mutex: &parking_lot::Mutex<T>) {
|
|
unsafe { rustpython_common::lock::zero_reinit_after_fork(mutex.raw()) };
|
|
}
|
|
|
|
/// Reset a parking_lot::Condvar after fork.
|
|
///
|
|
/// `notify_all()` would unpark waiters that did not survive the fork.
|
|
#[cfg(all(unix, feature = "host_env"))]
|
|
fn reinit_parking_lot_condvar(cvar: &parking_lot::Condvar) {
|
|
unsafe { rustpython_common::lock::zero_reinit_after_fork(cvar) };
|
|
}
|
|
|
|
// Thread handle state enum
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ThreadHandleState {
|
|
NotStarted,
|
|
Starting,
|
|
Running,
|
|
Done,
|
|
}
|
|
|
|
// Internal shared state for thread handle
|
|
pub struct ThreadHandleInner {
|
|
pub state: ThreadHandleState,
|
|
pub ident: u64,
|
|
pub join_handle: Option<thread::JoinHandle<()>>,
|
|
pub joining: bool, // True if a thread is currently joining
|
|
pub joined: bool, // Track if join has completed
|
|
}
|
|
|
|
impl fmt::Debug for ThreadHandleInner {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("ThreadHandleInner")
|
|
.field("state", &self.state)
|
|
.field("ident", &self.ident)
|
|
.field("join_handle", &self.join_handle.is_some())
|
|
.field("joining", &self.joining)
|
|
.field("joined", &self.joined)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
// _ThreadHandle - handle for joinable threads
|
|
#[pyattr]
|
|
#[pyclass(module = "_thread", name = "_ThreadHandle")]
|
|
#[derive(Debug, PyPayload)]
|
|
struct ThreadHandle {
|
|
inner: Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
|
// Event to signal thread completion (for timed join support)
|
|
done_event: Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
}
|
|
|
|
impl ThreadHandle {
|
|
fn new(vm: &VirtualMachine) -> Self {
|
|
let inner = Arc::new(parking_lot::Mutex::new(ThreadHandleInner {
|
|
state: ThreadHandleState::NotStarted,
|
|
ident: 0,
|
|
join_handle: None,
|
|
joining: false,
|
|
joined: false,
|
|
}));
|
|
let done_event =
|
|
Arc::new((parking_lot::Mutex::new(false), parking_lot::Condvar::new()));
|
|
|
|
// Register in global registry for fork cleanup
|
|
vm.state
|
|
.thread_handles
|
|
.lock()
|
|
.push((Arc::downgrade(&inner), Arc::downgrade(&done_event)));
|
|
|
|
Self { inner, done_event }
|
|
}
|
|
|
|
fn join_internal(
|
|
inner: &Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
|
done_event: &Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
timeout_duration: Option<Duration>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
Self::check_started(inner, vm)?;
|
|
|
|
let deadline =
|
|
timeout_duration.and_then(|timeout| std::time::Instant::now().checked_add(timeout));
|
|
|
|
// Wait for thread completion using Condvar (supports timeout)
|
|
// Loop to handle spurious wakeups
|
|
let (lock, cvar) = &**done_event;
|
|
let mut done = lock_done(lock, vm);
|
|
|
|
// ThreadHandle_join semantics: self-join/finalizing checks
|
|
// apply only while target thread has not reported it is exiting yet.
|
|
if !*done {
|
|
let inner_guard = inner.lock();
|
|
let current_ident = get_ident();
|
|
if inner_guard.ident == current_ident
|
|
&& inner_guard.state == ThreadHandleState::Running
|
|
{
|
|
return Err(vm.new_runtime_error("Cannot join current thread"));
|
|
}
|
|
if vm
|
|
.state
|
|
.finalizing
|
|
.load(core::sync::atomic::Ordering::Acquire)
|
|
{
|
|
return Err(vm.new_exception_msg(
|
|
vm.ctx.exceptions.python_finalization_error.to_owned(),
|
|
"cannot join thread at interpreter shutdown"
|
|
.to_owned()
|
|
.into(),
|
|
));
|
|
}
|
|
}
|
|
|
|
while !*done {
|
|
if let Some(timeout) = timeout_duration {
|
|
let remaining = deadline.map_or(timeout, |deadline| {
|
|
deadline.saturating_duration_since(std::time::Instant::now())
|
|
});
|
|
if remaining.is_zero() {
|
|
return Ok(());
|
|
}
|
|
let result = vm.allow_threads(|| cvar.wait_for(&mut done, remaining));
|
|
if result.timed_out() && !*done {
|
|
// Timeout occurred and done is still false
|
|
return Ok(());
|
|
}
|
|
} else {
|
|
// Infinite wait
|
|
vm.allow_threads(|| cvar.wait(&mut done));
|
|
}
|
|
}
|
|
drop(done);
|
|
|
|
// Thread is done, now perform cleanup
|
|
let join_handle = {
|
|
let mut inner_guard = inner.lock();
|
|
|
|
// If already joined, return immediately (idempotent)
|
|
if inner_guard.joined {
|
|
return Ok(());
|
|
}
|
|
|
|
// If another thread is already joining, wait for them to finish
|
|
if inner_guard.joining {
|
|
drop(inner_guard);
|
|
// Wait on done_event
|
|
let (lock, cvar) = &**done_event;
|
|
let mut done = lock_done(lock, vm);
|
|
while !*done {
|
|
vm.allow_threads(|| cvar.wait(&mut done));
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
// Mark that we're joining
|
|
inner_guard.joining = true;
|
|
|
|
// Take the join handle if available
|
|
inner_guard.join_handle.take()
|
|
};
|
|
|
|
// Perform the actual join outside the lock
|
|
if let Some(handle) = join_handle {
|
|
// Ignore the result - panics in spawned threads are already handled
|
|
let _ = vm.allow_threads(|| handle.join());
|
|
}
|
|
|
|
// Mark as joined and clear joining flag
|
|
{
|
|
let mut inner_guard = inner.lock();
|
|
inner_guard.joined = true;
|
|
inner_guard.joining = false;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn check_started(
|
|
inner: &Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
let state = inner.lock().state;
|
|
if matches!(
|
|
state,
|
|
ThreadHandleState::NotStarted | ThreadHandleState::Starting
|
|
) {
|
|
return Err(vm.new_runtime_error("thread not started"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn set_done_internal(
|
|
inner: &Arc<parking_lot::Mutex<ThreadHandleInner>>,
|
|
done_event: &Arc<(parking_lot::Mutex<bool>, parking_lot::Condvar)>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<()> {
|
|
Self::check_started(inner, vm)?;
|
|
{
|
|
let mut inner_guard = inner.lock();
|
|
inner_guard.state = ThreadHandleState::Done;
|
|
// _set_done() detach path. Dropping the JoinHandle
|
|
// detaches the underlying Rust thread.
|
|
inner_guard.join_handle = None;
|
|
inner_guard.joining = false;
|
|
inner_guard.joined = true;
|
|
}
|
|
remove_from_shutdown_handles(vm, inner, done_event);
|
|
|
|
let (lock, cvar) = &**done_event;
|
|
*lock_done(lock, vm) = true;
|
|
cvar.notify_all();
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_join_timeout(
|
|
timeout_obj: Option<crate::PyObjectRef>,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<Option<Duration>> {
|
|
const JOIN_TIMEOUT_MAX_SECONDS: i64 = TIMEOUT_MAX_IN_MICROSECONDS / 1_000_000;
|
|
let Some(timeout_obj) = timeout_obj else {
|
|
return Ok(None);
|
|
};
|
|
|
|
if let Some(t) = timeout_obj.try_index_opt(vm) {
|
|
let t: i64 = t?.try_to_primitive(vm).map_err(|_| {
|
|
vm.new_overflow_error("timestamp too large to convert to C PyTime_t")
|
|
})?;
|
|
if !(-JOIN_TIMEOUT_MAX_SECONDS..=JOIN_TIMEOUT_MAX_SECONDS).contains(&t) {
|
|
return Err(
|
|
vm.new_overflow_error("timestamp too large to convert to C PyTime_t")
|
|
);
|
|
}
|
|
if t < 0 {
|
|
return Ok(None);
|
|
}
|
|
return Ok(Some(Duration::from_secs(t as u64)));
|
|
}
|
|
|
|
if let Some(t) = timeout_obj.try_float_opt(vm) {
|
|
let t = t?.to_f64();
|
|
if t.is_nan() {
|
|
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
|
|
}
|
|
if !t.is_finite() || !(-TIMEOUT_MAX..=TIMEOUT_MAX).contains(&t) {
|
|
return Err(vm.new_overflow_error("timestamp out of range for platform time_t"));
|
|
}
|
|
if t < 0.0 {
|
|
return Ok(None);
|
|
}
|
|
return Ok(Some(Duration::from_secs_f64(t)));
|
|
}
|
|
|
|
Err(vm.new_type_error(format!(
|
|
"'{}' object cannot be interpreted as an integer or float",
|
|
timeout_obj.class().name()
|
|
)))
|
|
}
|
|
}
|
|
|
|
#[pyclass(with(Representable))]
|
|
impl Py<ThreadHandle> {
|
|
#[pygetset]
|
|
fn ident(&self) -> u64 {
|
|
self.inner.lock().ident
|
|
}
|
|
|
|
#[pymethod]
|
|
fn is_done(&self, vm: &VirtualMachine) -> PyResult<bool> {
|
|
// If completion was observed, perform one-time join cleanup
|
|
// before returning True.
|
|
let done = {
|
|
let (lock, _) = &*self.done_event;
|
|
*lock_done(lock, vm)
|
|
};
|
|
if !done {
|
|
return Ok(false);
|
|
}
|
|
ThreadHandle::join_internal(&self.inner, &self.done_event, Some(Duration::ZERO), vm)?;
|
|
Ok(true)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn _set_done(&self, vm: &VirtualMachine) -> PyResult<()> {
|
|
ThreadHandle::set_done_internal(&self.inner, &self.done_event, vm)
|
|
}
|
|
|
|
#[pymethod]
|
|
fn join(&self, mut f_args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
|
|
if !f_args.kwargs.is_empty() {
|
|
return Err(vm.new_type_error("_ThreadHandle.join() takes no keyword arguments"));
|
|
}
|
|
let given = f_args.args.len();
|
|
if given > 1 {
|
|
return Err(
|
|
vm.new_type_error(format!("join() takes at most 1 argument ({given} given)"))
|
|
);
|
|
}
|
|
let timeout = f_args.take_positional().filter(|obj| !vm.is_none(obj));
|
|
let timeout_duration = ThreadHandle::parse_join_timeout(timeout, vm)?;
|
|
ThreadHandle::join_internal(&self.inner, &self.done_event, timeout_duration, vm)
|
|
}
|
|
|
|
#[pyslot]
|
|
fn slot_new(cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
|
ThreadHandle::new(vm)
|
|
.into_ref_with_type(vm, cls)
|
|
.map(Into::into)
|
|
}
|
|
}
|
|
|
|
impl Representable for ThreadHandle {
|
|
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
|
|
let ident = zelf.inner.lock().ident;
|
|
Ok(format!(
|
|
"<{} object: ident={ident}>",
|
|
zelf.class().slot_name()
|
|
))
|
|
}
|
|
}
|
|
|
|
struct StartJoinableThreadArgs {
|
|
function_obj: PyObjectRef,
|
|
function: ArgCallable,
|
|
handle: Option<PyRef<ThreadHandle>>,
|
|
daemon: bool,
|
|
}
|
|
|
|
impl FromArgs for StartJoinableThreadArgs {
|
|
const PARAMS: Option<&'static [Param]> = Some(&[
|
|
Param::positional_or_keyword("function"),
|
|
Param {
|
|
name: "handle",
|
|
kind: ParamKind::PositionalOrKeyword,
|
|
default: Some(DefaultRepr::None),
|
|
},
|
|
Param {
|
|
name: "daemon",
|
|
kind: ParamKind::PositionalOrKeyword,
|
|
default: Some(DefaultRepr::Bool(true)),
|
|
},
|
|
]);
|
|
|
|
fn from_args(vm: &VirtualMachine, f_args: &mut FuncArgs) -> Result<Self, ArgumentError> {
|
|
let given = f_args.args.len() + f_args.kwargs.len();
|
|
if given > 3 {
|
|
return Err(vm
|
|
.new_type_error(format!(
|
|
"start_joinable_thread() takes at most 3 arguments ({given} given)"
|
|
))
|
|
.into());
|
|
}
|
|
|
|
let function_pos = f_args.take_positional();
|
|
let function_kw = f_args.take_keyword("function");
|
|
if function_pos.is_some() && function_kw.is_some() {
|
|
return Err(vm
|
|
.new_type_error(
|
|
"argument for start_joinable_thread() given by name ('function') and position (1)",
|
|
)
|
|
.into());
|
|
}
|
|
let Some(function_obj) = function_pos.or(function_kw) else {
|
|
return Err(vm
|
|
.new_type_error(
|
|
"start_joinable_thread() missing required argument 'function' (pos 1)",
|
|
)
|
|
.into());
|
|
};
|
|
|
|
let handle_pos = f_args.take_positional();
|
|
let handle_kw = f_args.take_keyword("handle");
|
|
if handle_pos.is_some() && handle_kw.is_some() {
|
|
return Err(vm
|
|
.new_type_error(
|
|
"argument for start_joinable_thread() given by name ('handle') and position (2)",
|
|
)
|
|
.into());
|
|
}
|
|
let handle_obj = handle_pos.or(handle_kw);
|
|
|
|
let daemon_pos = f_args.take_positional();
|
|
let daemon_kw = f_args.take_keyword("daemon");
|
|
if daemon_pos.is_some() && daemon_kw.is_some() {
|
|
return Err(vm
|
|
.new_type_error(
|
|
"argument for start_joinable_thread() given by name ('daemon') and position (3)",
|
|
)
|
|
.into());
|
|
}
|
|
let daemon = daemon_pos
|
|
.or(daemon_kw)
|
|
.map_or(Ok(true), |obj| obj.try_to_bool(vm))?;
|
|
|
|
// Required argument errors are raised before unknown keyword errors
|
|
// when `function` is missing.
|
|
if let Some(unexpected) = f_args.kwargs.keys().next() {
|
|
let suggestion = ["function", "handle", "daemon"]
|
|
.iter()
|
|
.filter_map(|candidate| {
|
|
let max_distance = (unexpected.len() + candidate.len() + 3) * MOVE_COST / 6;
|
|
let distance = levenshtein_distance(
|
|
unexpected.as_bytes(),
|
|
candidate.as_bytes(),
|
|
max_distance,
|
|
);
|
|
(distance <= max_distance).then_some((distance, *candidate))
|
|
})
|
|
.min_by_key(|(distance, _)| *distance)
|
|
.map(|(_, candidate)| candidate);
|
|
|
|
let msg_suffix =
|
|
suggestion.map_or_else(String::new, |s| format!(". Did you mean '{s}'?"));
|
|
let msg = format!(
|
|
"start_joinable_thread() got an unexpected keyword argument '{unexpected}'{msg_suffix}"
|
|
);
|
|
return Err(vm.new_type_error(msg).into());
|
|
}
|
|
|
|
if function_obj.to_callable().is_none() {
|
|
return Err(vm.new_type_error("thread function must be callable").into());
|
|
}
|
|
let function: ArgCallable = function_obj.clone().try_into_value(vm)?;
|
|
|
|
let thread_handle_type = ThreadHandle::class(&vm.ctx);
|
|
let handle = if let Some(handle_obj) = handle_obj {
|
|
if vm.is_none(&handle_obj) {
|
|
None
|
|
} else if !handle_obj.class().is(thread_handle_type) {
|
|
return Err(vm.new_type_error("'handle' must be a _ThreadHandle").into());
|
|
} else {
|
|
Some(
|
|
handle_obj
|
|
.downcast::<ThreadHandle>()
|
|
.map_err(|_| vm.new_type_error("'handle' must be a _ThreadHandle"))?,
|
|
)
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
Ok(Self {
|
|
function_obj,
|
|
function,
|
|
handle,
|
|
daemon,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[pyfunction]
|
|
fn start_joinable_thread(
|
|
parsed: StartJoinableThreadArgs,
|
|
vm: &VirtualMachine,
|
|
) -> PyResult<PyRef<ThreadHandle>> {
|
|
let StartJoinableThreadArgs {
|
|
function_obj,
|
|
function,
|
|
handle,
|
|
daemon,
|
|
} = parsed;
|
|
|
|
vm.audit("_thread.start_joinable_thread", || {
|
|
(
|
|
function_obj,
|
|
daemon,
|
|
handle
|
|
.as_ref()
|
|
.map_or_else(|| vm.ctx.none(), |h| h.clone().into()),
|
|
)
|
|
})?;
|
|
|
|
if !vm.state.allow_threads() {
|
|
return Err(vm.new_runtime_error(
|
|
"thread is not supported for isolated subinterpreters".to_owned(),
|
|
));
|
|
}
|
|
if vm
|
|
.state
|
|
.finalizing
|
|
.load(core::sync::atomic::Ordering::Acquire)
|
|
{
|
|
return Err(vm.new_exception_msg(
|
|
vm.ctx.exceptions.python_finalization_error.to_owned(),
|
|
"can't create new thread at interpreter shutdown"
|
|
.to_owned()
|
|
.into(),
|
|
));
|
|
}
|
|
|
|
let handle = match handle {
|
|
Some(h) => h,
|
|
None => ThreadHandle::new(vm).into_ref(&vm.ctx),
|
|
};
|
|
|
|
// Must only start once (ThreadHandle_start).
|
|
{
|
|
let mut inner = handle.inner.lock();
|
|
if inner.state != ThreadHandleState::NotStarted {
|
|
return Err(vm.new_runtime_error("thread already started"));
|
|
}
|
|
inner.state = ThreadHandleState::Starting;
|
|
inner.ident = 0;
|
|
inner.join_handle = None;
|
|
inner.joining = false;
|
|
inner.joined = false;
|
|
}
|
|
// Starting a handle always resets the completion event.
|
|
{
|
|
let (done_lock, _) = &*handle.done_event;
|
|
*lock_done(done_lock, vm) = false;
|
|
}
|
|
|
|
// Add non-daemon threads to shutdown registry so _shutdown() will wait for them
|
|
if !daemon {
|
|
add_to_shutdown_handles(vm, &handle.inner, &handle.done_event);
|
|
}
|
|
|
|
let func = function;
|
|
let handle_clone = handle.clone();
|
|
let inner_clone = handle.inner.clone();
|
|
let done_event_clone = handle.done_event.clone();
|
|
// Use std::sync (pthread-based) instead of parking_lot for these
|
|
// events so they remain fork-safe without the parking_lot_core patch.
|
|
let started_event = Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
|
|
let started_event_clone = Arc::clone(&started_event);
|
|
let handle_ready_event =
|
|
Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
|
|
let handle_ready_event_clone = Arc::clone(&handle_ready_event);
|
|
|
|
let thread_builder = apply_thread_stack_size(thread::Builder::new(), vm);
|
|
|
|
let join_handle = thread_builder
|
|
.spawn(vm.new_thread().make_spawn_func(move |vm| {
|
|
// Publish ident for the parent starter thread.
|
|
{
|
|
inner_clone.lock().ident = get_ident();
|
|
}
|
|
{
|
|
let (started_lock, started_cvar) = &*started_event_clone;
|
|
*started_lock.lock().unwrap() = true;
|
|
started_cvar.notify_all();
|
|
}
|
|
// Don't execute the target function until parent marks the
|
|
// handle as running. Detach while blocked so a concurrent
|
|
// stop-the-world (e.g. a GC on another thread) can park this
|
|
// thread instead of stalling waiting for it to reach a
|
|
// safepoint it will not reach until released.
|
|
{
|
|
let (ready_lock, ready_cvar) = &*handle_ready_event_clone;
|
|
vm.allow_threads(|| {
|
|
let mut ready = ready_lock.lock().unwrap();
|
|
while !*ready {
|
|
// Short timeout so we stay responsive to STW requests.
|
|
let (guard, _) = ready_cvar
|
|
.wait_timeout(ready, core::time::Duration::from_millis(1))
|
|
.unwrap();
|
|
ready = guard;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Ensure cleanup happens even if the function panics
|
|
let inner_for_cleanup = inner_clone.clone();
|
|
let done_event_for_cleanup = done_event_clone.clone();
|
|
let vm_state = vm.state.clone();
|
|
scopeguard::defer! {
|
|
// Mark as done
|
|
inner_for_cleanup.lock().state = ThreadHandleState::Done;
|
|
|
|
// Handle sentinels
|
|
for lock in SENTINELS.take() {
|
|
if lock.mu.is_locked() {
|
|
unsafe { lock.mu.unlock() };
|
|
}
|
|
}
|
|
|
|
// Clean up thread-local data while VM context is still active
|
|
cleanup_thread_local_data();
|
|
|
|
// Clean up frame tracking
|
|
crate::vm::thread::cleanup_current_thread_frames(vm);
|
|
|
|
vm_state.thread_count.fetch_sub(1);
|
|
|
|
// The runtime no longer needs to wait for this thread.
|
|
remove_from_shutdown_handles(vm, &inner_for_cleanup, &done_event_for_cleanup);
|
|
|
|
// Signal waiting threads that this thread is done
|
|
// This must be LAST to ensure all cleanup is complete before join() returns
|
|
{
|
|
let (lock, cvar) = &*done_event_for_cleanup;
|
|
*lock_done(lock, vm) = true;
|
|
cvar.notify_all();
|
|
}
|
|
}
|
|
|
|
// Increment thread count when thread actually starts executing
|
|
vm_state.thread_count.fetch_add(1);
|
|
|
|
// 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)
|
|
{
|
|
report_unraisable_thread_exception(exc, &func, vm);
|
|
}
|
|
}
|
|
}))
|
|
.map_err(|_err| {
|
|
// force_done + remove_from_shutdown_handles on start failure.
|
|
{
|
|
let mut inner = handle.inner.lock();
|
|
inner.state = ThreadHandleState::Done;
|
|
inner.join_handle = None;
|
|
inner.joining = false;
|
|
inner.joined = true;
|
|
}
|
|
{
|
|
let (done_lock, done_cvar) = &*handle.done_event;
|
|
*lock_done(done_lock, vm) = true;
|
|
done_cvar.notify_all();
|
|
}
|
|
if !daemon {
|
|
remove_from_shutdown_handles(vm, &handle.inner, &handle.done_event);
|
|
}
|
|
vm.new_runtime_error("can't start new thread")
|
|
})?;
|
|
|
|
// Wait until the new thread has reported its ident. Detach while
|
|
// waiting so a concurrent stop-the-world (e.g. a GC on another thread)
|
|
// can park this thread instead of stalling on it: the child may park
|
|
// itself at startup while the world is stopped and cannot report until
|
|
// released, so the waiter must be parkable too.
|
|
{
|
|
let (started_lock, started_cvar) = &*started_event;
|
|
vm.allow_threads(|| {
|
|
let mut started = started_lock.lock().unwrap();
|
|
while !*started {
|
|
let (guard, _) = started_cvar
|
|
.wait_timeout(started, core::time::Duration::from_millis(1))
|
|
.unwrap();
|
|
started = guard;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Mark the handle running in the parent thread (like CPython's
|
|
// ThreadHandle_start sets THREAD_HANDLE_RUNNING after spawn succeeds).
|
|
{
|
|
let mut inner = handle.inner.lock();
|
|
inner.join_handle = Some(join_handle);
|
|
inner.state = ThreadHandleState::Running;
|
|
}
|
|
|
|
// Unblock the started thread once handle state is fully published.
|
|
{
|
|
let (ready_lock, ready_cvar) = &*handle_ready_event;
|
|
*ready_lock.lock().unwrap() = true;
|
|
ready_cvar.notify_all();
|
|
}
|
|
|
|
Ok(handle_clone)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[cfg(any(
|
|
all(unix, feature = "host_env"),
|
|
all(debug_assertions, any(target_os = "linux", target_os = "macos"))
|
|
))]
|
|
use super::*;
|
|
#[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))]
|
|
use crate::Interpreter;
|
|
|
|
/// Exercise only the native completion-state invariant, without Python
|
|
/// execution or actual fork in the multithreaded Rust test harness.
|
|
#[test]
|
|
#[cfg(all(unix, feature = "host_env"))]
|
|
fn fork_reinitializes_surviving_handle_completion_lock() {
|
|
for (ident, state, completed, expected_state, expected_completed) in [
|
|
(
|
|
7,
|
|
ThreadHandleState::Running,
|
|
false,
|
|
ThreadHandleState::Running,
|
|
false,
|
|
),
|
|
(
|
|
0,
|
|
ThreadHandleState::NotStarted,
|
|
false,
|
|
ThreadHandleState::NotStarted,
|
|
false,
|
|
),
|
|
(
|
|
8,
|
|
ThreadHandleState::Running,
|
|
false,
|
|
ThreadHandleState::Done,
|
|
true,
|
|
),
|
|
(
|
|
8,
|
|
ThreadHandleState::Done,
|
|
true,
|
|
ThreadHandleState::Done,
|
|
true,
|
|
),
|
|
] {
|
|
let inner = parking_lot::Mutex::new(ThreadHandleInner {
|
|
state,
|
|
ident,
|
|
join_handle: None,
|
|
joining: false,
|
|
joined: false,
|
|
});
|
|
let done_event = (
|
|
parking_lot::Mutex::new(completed),
|
|
parking_lot::Condvar::new(),
|
|
);
|
|
// Model the bytes inherited from a parent joiner that held the
|
|
// mutex at fork. No guard remains live and no other thread can
|
|
// access these exclusively owned fixtures.
|
|
core::mem::forget(done_event.0.lock());
|
|
assert!(done_event.0.try_lock().is_none());
|
|
// SAFETY: these private fixtures have no live guards/waiters,
|
|
// exactly as in a child after its parent lock owner vanished.
|
|
unsafe { reinit_handle_after_fork(&inner, &done_event, 7) };
|
|
let completed = done_event
|
|
.0
|
|
.try_lock()
|
|
.expect("copied completion lock must be unlocked");
|
|
assert_eq!(*completed, expected_completed);
|
|
let inner = inner.lock();
|
|
assert_eq!(inner.state, expected_state);
|
|
assert_eq!(inner.ident, ident);
|
|
}
|
|
}
|
|
|
|
/// Regression test for #7941: a Python thread started without an
|
|
/// explicit `threading.stack_size()` must not run on Rust's 2 MiB
|
|
/// std default in debug builds, where the call chains the stdlib
|
|
/// runs on helper threads (e.g. the SSL test server) overflowed it.
|
|
#[test]
|
|
#[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))]
|
|
fn default_python_thread_stack_size_debug() {
|
|
Interpreter::without_stdlib(Default::default()).enter(|vm| {
|
|
assert_eq!(vm.state.stacksize.load(), 0);
|
|
let builder = apply_thread_stack_size(thread::Builder::new(), vm);
|
|
let stack_size = builder
|
|
.spawn(current_thread_stack_size)
|
|
.expect("failed to spawn thread")
|
|
.join()
|
|
.expect("thread panicked");
|
|
assert!(
|
|
stack_size >= DEFAULT_THREAD_STACK_SIZE,
|
|
"Python thread stack size is {stack_size} bytes, expected at least {DEFAULT_THREAD_STACK_SIZE}"
|
|
);
|
|
});
|
|
}
|
|
|
|
/// A size small enough for CPython's frames is not small enough for an
|
|
/// unoptimized build's: `test_threading` asks for 256 KiB, which holds
|
|
/// three of them where starting a thread needs six. The size the
|
|
/// request set is still what `threading.stack_size()` answers with.
|
|
#[test]
|
|
#[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))]
|
|
fn explicit_python_thread_stack_size_is_a_floor_debug() {
|
|
const REQUESTED: usize = 256 * 1024;
|
|
|
|
Interpreter::without_stdlib(Default::default()).enter(|vm| {
|
|
vm.state.stacksize.store(REQUESTED);
|
|
let builder = apply_thread_stack_size(thread::Builder::new(), vm);
|
|
let stack_size = builder
|
|
.spawn(current_thread_stack_size)
|
|
.expect("failed to spawn thread")
|
|
.join()
|
|
.expect("thread panicked");
|
|
assert!(
|
|
stack_size >= DEFAULT_THREAD_STACK_SIZE,
|
|
"Python thread stack size is {stack_size} bytes, expected at least {DEFAULT_THREAD_STACK_SIZE}"
|
|
);
|
|
assert_eq!(vm.state.stacksize.load(), REQUESTED);
|
|
});
|
|
}
|
|
|
|
#[cfg(all(debug_assertions, target_os = "linux"))]
|
|
fn current_thread_stack_size() -> usize {
|
|
use libc::{
|
|
pthread_attr_destroy, pthread_attr_getstacksize, pthread_attr_t,
|
|
pthread_getattr_np, pthread_self,
|
|
};
|
|
let mut attr: pthread_attr_t = unsafe { core::mem::zeroed() };
|
|
unsafe {
|
|
assert_eq!(pthread_getattr_np(pthread_self(), &mut attr), 0);
|
|
let mut size = 0;
|
|
assert_eq!(pthread_attr_getstacksize(&attr, &mut size), 0);
|
|
pthread_attr_destroy(&mut attr);
|
|
size
|
|
}
|
|
}
|
|
|
|
#[cfg(all(debug_assertions, target_os = "macos"))]
|
|
fn current_thread_stack_size() -> usize {
|
|
unsafe { libc::pthread_get_stacksize_np(libc::pthread_self()) }
|
|
}
|
|
}
|
|
}
|