@@ -135,9 +135,7 @@ impl GcGeneration {
135135/// empties its own interpreter's objects; another interpreter's stay behind with
136136/// the count already zeroed, and untracking one of those must not wrap.
137137fn release_count ( count : & AtomicUsize ) {
138- if count. load ( Ordering :: Relaxed ) > 0 {
139- count. fetch_sub ( 1 , Ordering :: Relaxed ) ;
140- }
138+ let _ = count. try_update ( Ordering :: Relaxed , Ordering :: Relaxed , |n| n. checked_sub ( 1 ) ) ;
141139}
142140
143141/// Whether `owner`'s collections act on `obj`.
@@ -552,7 +550,7 @@ impl GcState {
552550 // (e.g. a lazily-initialized frame locals cell) that another
553551 // thread is blocked on with no way to reach a safepoint —
554552 // a deadlock. At a safepoint no such lock is held.
555- crate :: signal :: schedule_gc ( ) ;
553+ gc . scheduled . store ( true , Ordering :: Relaxed ) ;
556554 return false ;
557555 }
558556 // Without threading there is no safepoint to defer to and no other
@@ -574,6 +572,8 @@ impl GcState {
574572 force : bool ,
575573 ) -> CollectResult {
576574 if !force && !gc. is_enabled ( ) {
575+ #[ cfg( feature = "threading" ) ]
576+ gc. scheduled . store ( false , Ordering :: Relaxed ) ;
577577 return CollectResult :: default ( ) ;
578578 }
579579
@@ -582,6 +582,12 @@ impl GcState {
582582 return CollectResult :: default ( ) ;
583583 } ;
584584
585+ // A busy collector must not consume another interpreter's request.
586+ // Clear only after acquiring the lock, before callbacks can request
587+ // a later collection.
588+ #[ cfg( feature = "threading" ) ]
589+ gc. scheduled . store ( false , Ordering :: Relaxed ) ;
590+
585591 let start_time = cfg_select ! {
586592 target_arch = "wasm32" => ( ) ,
587593 _ => std:: time:: Instant :: now( ) ,
@@ -1268,6 +1274,9 @@ pub struct GcInterpreterState {
12681274 pub generations : [ GcGeneration ; 3 ] ,
12691275 /// GC enabled flag
12701276 enabled : AtomicBool ,
1277+ /// Automatic collection requested at this interpreter's next safepoint.
1278+ #[ cfg( feature = "threading" ) ]
1279+ scheduled : AtomicBool ,
12711280 /// Debug flags
12721281 debug : AtomicU32 ,
12731282 /// Uncollectable objects saved by this interpreter's collections, drained
@@ -1289,6 +1298,8 @@ impl GcInterpreterState {
12891298 GcGeneration :: new ( 0 ) , // old[1]
12901299 ] ,
12911300 enabled : AtomicBool :: new ( true ) ,
1301+ #[ cfg( feature = "threading" ) ]
1302+ scheduled : AtomicBool :: new ( false ) ,
12921303 debug : AtomicU32 :: new ( 0 ) ,
12931304 garbage : PyMutex :: new ( Vec :: new ( ) ) ,
12941305 py_garbage : ctx. new_list ( Vec :: new ( ) ) ,
@@ -1304,6 +1315,14 @@ impl GcInterpreterState {
13041315 self . enabled . load ( Ordering :: Relaxed )
13051316 }
13061317
1318+ /// Leave requests pending while a collector is busy, without repeatedly
1319+ /// entering the bytecode loop's slow path during its Python callbacks.
1320+ #[ cfg( feature = "threading" ) ]
1321+ #[ inline]
1322+ pub ( crate ) fn collection_ready ( & self ) -> bool {
1323+ self . scheduled . load ( Ordering :: Relaxed ) && !gc_state ( ) . collecting . is_locked ( )
1324+ }
1325+
13071326 /// Enable GC
13081327 pub fn enable ( & self ) {
13091328 self . enabled . store ( true , Ordering :: Relaxed ) ;
@@ -1524,4 +1543,76 @@ mod tests {
15241543 assert_ne ! ( first. owner, GC_NO_OWNER ) ;
15251544 assert_ne ! ( second. owner, GC_NO_OWNER ) ;
15261545 }
1546+
1547+ #[ cfg( feature = "threading" ) ]
1548+ #[ test]
1549+ fn automatic_gc_request_stays_with_allocating_interpreter ( ) {
1550+ let first = crate :: Interpreter :: without_stdlib ( Default :: default ( ) ) ;
1551+ let second = crate :: Interpreter :: without_stdlib ( Default :: default ( ) ) ;
1552+ first. enter ( |vm| vm. state . gc . scheduled . store ( false , Ordering :: Relaxed ) ) ;
1553+ second. enter ( |vm| vm. state . gc . scheduled . store ( false , Ordering :: Relaxed ) ) ;
1554+ // An isolated allocation counter makes crossing the threshold
1555+ // deterministic without depending on the rest of the test process.
1556+ let allocations = GcState :: new ( ) ;
1557+ allocations. counts [ 0 ] . store ( 1 , Ordering :: Relaxed ) ;
1558+ first. enter ( |vm| {
1559+ vm. state . gc . set_threshold ( 1 , None , None ) ;
1560+ assert ! ( !allocations. maybe_collect( & vm. state. gc) ) ;
1561+ } ) ;
1562+ second. enter ( |vm| {
1563+ assert ! ( !vm. state. gc. scheduled. load( Ordering :: Relaxed ) ) ;
1564+ vm. run_scheduled_gc ( ) ;
1565+ } ) ;
1566+ first. enter ( |vm| {
1567+ assert ! ( vm. state. gc. scheduled. swap( false , Ordering :: Relaxed ) ) ;
1568+ } ) ;
1569+ }
1570+
1571+ #[ cfg( feature = "threading" ) ]
1572+ #[ test]
1573+ fn automatic_gc_request_survives_busy_collector ( ) {
1574+ let state = interpreter_state ( ) ;
1575+ let _guard = gc_state ( ) . collecting . lock ( ) ;
1576+ state. scheduled . store ( true , Ordering :: Relaxed ) ;
1577+ state. collect ( 0 ) ;
1578+ assert ! ( state. scheduled. load( Ordering :: Relaxed ) ) ;
1579+ assert ! ( !state. collection_ready( ) ) ;
1580+
1581+ state. disable ( ) ;
1582+ state. collect ( 0 ) ;
1583+ assert ! ( !state. scheduled. load( Ordering :: Relaxed ) ) ;
1584+ }
1585+
1586+ #[ test]
1587+ fn release_count_does_not_wrap_during_reset ( ) {
1588+ use std:: sync:: Barrier ;
1589+
1590+ let count = AtomicUsize :: new ( 1 ) ;
1591+ let start = Barrier :: new ( 3 ) ;
1592+ let finish = Barrier :: new ( 3 ) ;
1593+ let mut underflows = 0 ;
1594+ std:: thread:: scope ( |scope| {
1595+ for reset in [ false , true ] {
1596+ let ( count, start, finish) = ( & count, & start, & finish) ;
1597+ scope. spawn ( move || {
1598+ for _ in 0 ..10_000 {
1599+ start. wait ( ) ;
1600+ if reset {
1601+ count. store ( 0 , Ordering :: Relaxed ) ;
1602+ } else {
1603+ release_count ( count) ;
1604+ }
1605+ finish. wait ( ) ;
1606+ }
1607+ } ) ;
1608+ }
1609+ for _ in 0 ..10_000 {
1610+ count. store ( 1 , Ordering :: Relaxed ) ;
1611+ start. wait ( ) ;
1612+ finish. wait ( ) ;
1613+ underflows += usize:: from ( count. load ( Ordering :: Relaxed ) > 1 ) ;
1614+ }
1615+ } ) ;
1616+ assert_eq ! ( underflows, 0 ) ;
1617+ }
15271618}
0 commit comments