Skip to content
Draft

gc #7

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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -4810,11 +4810,15 @@ def run():
else:
self.assertFalse(err.strip('.!'))

# TODO: RUSTPYTHON; daemon thread exception during shutdown due to finalizing order change
@unittest.expectedFailure
@threading_helper.requires_working_threading()
@support.requires_resource('walltime')
def test_daemon_threads_shutdown_stdout_deadlock(self):
self.check_daemon_threads_shutdown_deadlock('stdout')

# TODO: RUSTPYTHON; daemon thread exception during shutdown due to finalizing order change
@unittest.expectedFailure
@threading_helper.requires_working_threading()
@support.requires_resource('walltime')
def test_daemon_threads_shutdown_stderr_deadlock(self):
Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ parking_lot = { workspace = true, optional = true }
unicode_names2 = { workspace = true }
radium = { workspace = true }

crossbeam-epoch = "0.9"
lock_api = "0.4"
siphasher = "1"
num-complex.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod boxvec;
pub mod cformat;
#[cfg(all(feature = "std", any(unix, windows, target_os = "wasi")))]
pub mod crt_fd;
pub use crossbeam_epoch as epoch;
pub mod encodings;
#[cfg(all(feature = "std", any(not(target_arch = "wasm32"), target_os = "wasi")))]
pub mod fileutils;
Expand Down
4 changes: 4 additions & 0 deletions crates/common/src/refcount.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
//! Reference counting implementation based on EBR (Epoch-Based Reclamation).

use crate::atomic::{Ordering, PyAtomic, Radium};

pub use crate::epoch::Guard;

// State layout (usize):
// [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [N bits: weak_count] [M bits: strong_count]
// 64-bit: N=30, M=31. 32-bit: N=14, M=15.
Expand Down
13 changes: 12 additions & 1 deletion crates/vm/src/gc_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,12 @@ impl GcState {
Err(_) => return (0, 0),
};

// Enter EBR critical section for the entire collection.
// This ensures that any objects being freed by other threads won't have
// their memory actually deallocated until we exit this critical section.
// Other threads' deferred deallocations will wait for us to unpin.
let ebr_guard = rustpython_common::epoch::pin();

// Memory barrier to ensure visibility of all reference count updates
// from other threads before we start analyzing the object graph.
core::sync::atomic::fence(Ordering::SeqCst);
Expand Down Expand Up @@ -694,7 +700,8 @@ impl GcState {
// during __del__ (step 6d) can still be upgraded.
//
// Clear and destroy objects within a deferred drop context.
// This prevents deadlocks from untrack calls during destruction.
// The ebr_guard ensures deferred deallocations from other threads wait for us.
// The deferred drop context prevents deadlocks from untrack calls during destruction.
rustpython_common::refcount::with_deferred_drops(|| {
for obj_ref in truly_dead.iter() {
if obj_ref.gc_has_clear() {
Expand All @@ -719,6 +726,10 @@ impl GcState {

self.generations[generation].update_stats(collected, 0);

// Flush EBR deferred operations before exiting collection.
// This ensures any deferred deallocations from this collection are executed.
ebr_guard.flush();

(collected, 0)
}

Expand Down
25 changes: 25 additions & 0 deletions crates/vm/src/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,34 @@ impl Drop for SignalHandlerGuard {
}
}

// Reactivate EBR guard every N instructions to prevent epoch starvation.
// This allows GC to advance epochs even during long-running operations.
// 65536 instructions ≈ 1ms, much faster than CPython's 5ms GIL timeout
#[cfg(all(feature = "threading", feature = "gc"))]
const REACTIVATE_INTERVAL: u32 = 65536;

#[cfg(all(feature = "threading", feature = "gc"))]
thread_local! {
static INSTRUCTION_COUNTER: core::cell::Cell<u32> = const { core::cell::Cell::new(0) };
}

#[cfg_attr(feature = "flame-it", flame)]
#[inline(always)]
pub fn check_signals(vm: &VirtualMachine) -> PyResult<()> {
// Periodic EBR guard reactivation to prevent epoch starvation
#[cfg(all(feature = "threading", feature = "gc"))]
{
INSTRUCTION_COUNTER.with(|counter| {
let count = counter.get();
if count >= REACTIVATE_INTERVAL {
crate::vm::thread::reactivate_guard();
counter.set(0);
} else {
counter.set(count + 1);
}
});
}

if vm.signal_handlers.get().is_none() {
return Ok(());
}
Expand Down
7 changes: 7 additions & 0 deletions crates/vm/src/stdlib/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,10 @@ pub(crate) mod _thread {
// Increment thread count when thread actually starts executing
vm.state.thread_count.fetch_add(1);

// Enter EBR critical section for this thread (Coarse-grained pinning)
// This ensures GC won't free objects while this thread might access them
crate::vm::thread::ensure_pinned();

match func.invoke(args, vm) {
Ok(_obj) => {}
Err(e) if e.fast_isinstance(vm.ctx.exceptions.system_exit) => {}
Expand All @@ -467,6 +471,9 @@ pub(crate) mod _thread {
// Clean up frame tracking
crate::vm::thread::cleanup_current_thread_frames(vm);
vm.state.thread_count.fetch_sub(1);

// Drop EBR guard when thread exits, allowing epoch advancement
crate::vm::thread::drop_guard();
}

/// Clean up thread-local data for the current thread.
Expand Down
5 changes: 5 additions & 0 deletions crates/vm/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,11 @@ impl VirtualMachine {
crate::vm::thread::pop_thread_frame();

self.recursion_depth.update(|d| d - 1);

// Reactivate EBR guard at frame boundary (safe point)
// This allows GC to advance epochs and free deferred objects
#[cfg(feature = "threading")]
crate::vm::thread::reactivate_guard();
}

use crate::protocol::TraceEvent;
Expand Down
41 changes: 41 additions & 0 deletions crates/vm/src/vm/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,51 @@ thread_local! {
pub(crate) static CURRENT_FRAME: AtomicPtr<Frame> =
const { AtomicPtr::new(core::ptr::null_mut()) };

/// Thread-local EBR guard for Coarse-grained pinning strategy.
/// Holds the EBR critical section guard for this thread.
pub(crate) static EBR_GUARD: RefCell<Option<rustpython_common::epoch::Guard>> =
const { RefCell::new(None) };
}

scoped_tls::scoped_thread_local!(static VM_CURRENT: VirtualMachine);

/// Ensure the current thread is pinned for EBR.
/// Call this at the start of operations that access Python objects.
///
/// This is part of the Coarse-grained pinning strategy where threads
/// are pinned at entry and periodically reactivate at safe points.
#[inline]
pub fn ensure_pinned() {
EBR_GUARD.with(|guard| {
if guard.borrow().is_none() {
*guard.borrow_mut() = Some(rustpython_common::epoch::pin());
}
});
}

/// Reactivate the EBR guard to allow epoch advancement.
/// Call this at safe points where no object references are held temporarily.
///
/// This unblocks GC from advancing epochs, allowing deferred objects to be freed.
/// The guard remains active after reactivation.
#[inline]
pub fn reactivate_guard() {
EBR_GUARD.with(|guard| {
if let Some(ref mut g) = *guard.borrow_mut() {
g.repin();
}
});
}

/// Drop the EBR guard, unpinning this thread.
/// Call this when the thread is exiting or no longer needs EBR protection.
#[inline]
pub fn drop_guard() {
EBR_GUARD.with(|guard| {
*guard.borrow_mut() = None;
});
}

pub fn with_current_vm<R>(f: impl FnOnce(&VirtualMachine) -> R) -> R {
if !VM_CURRENT.is_set() {
panic!("call with_current_vm() but VM_CURRENT is null");
Expand Down
Loading