cpython/Lib/test/test_free_threading/test_gc.py
Miss Islington (bot) d7505294df
[3.15] gh-148613: Fix race in gc_set_threshold and gc_get_threshold (GH-150356) (#150841)
gh-148613: Fix race in `gc_set_threshold` and `gc_get_threshold` (GH-150356)
(cherry picked from commit 41eb8ee2bb)

Co-authored-by: Edward Xu <xuxiangad@gmail.com>
2026-06-04 12:43:46 +05:30

129 lines
3.7 KiB
Python

import unittest
import threading
from threading import Thread
from unittest import TestCase
import gc
from test.support import threading_helper
class MyObj:
pass
@threading_helper.requires_working_threading()
class TestGC(TestCase):
def test_get_objects(self):
event = threading.Event()
def gc_thread():
for i in range(100):
o = gc.get_objects()
event.set()
def mutator_thread():
while not event.is_set():
o1 = MyObj()
o2 = MyObj()
o3 = MyObj()
o4 = MyObj()
gcs = [Thread(target=gc_thread)]
mutators = [Thread(target=mutator_thread) for _ in range(4)]
with threading_helper.start_threads(gcs + mutators):
pass
def test_get_referrers(self):
NUM_GC = 2
NUM_MUTATORS = 4
b = threading.Barrier(NUM_GC + NUM_MUTATORS)
event = threading.Event()
obj = MyObj()
def gc_thread():
b.wait()
for i in range(100):
o = gc.get_referrers(obj)
event.set()
def mutator_thread():
b.wait()
while not event.is_set():
d1 = { "key": obj }
d2 = { "key": obj }
d3 = { "key": obj }
d4 = { "key": obj }
gcs = [Thread(target=gc_thread) for _ in range(NUM_GC)]
mutators = [Thread(target=mutator_thread) for _ in range(NUM_MUTATORS)]
with threading_helper.start_threads(gcs + mutators):
pass
def test_freeze_object_in_brc_queue(self):
# GH-142975: Freezing objects in the BRC queue could result in some
# objects having a zero refcount without being deallocated.
class Weird:
# We need a destructor to trigger the check for object resurrection
def __del__(self):
pass
# This is owned by the main thread, so the subthread will have to increment
# this object's reference count.
weird = Weird()
def evil():
gc.freeze()
# Decrement the reference count from this thread, which will trigger the
# slow path during resurrection and add our weird object to the BRC queue.
nonlocal weird
del weird
# Collection will merge the object's reference count and make it zero.
gc.collect()
# Unfreeze the object, making it visible to the GC.
gc.unfreeze()
gc.collect()
thread = Thread(target=evil)
thread.start()
thread.join()
def test_set_threshold(self):
# GH-148613: Setting the GC threshold from another thread could cause a
# race between the `gc_should_collect` and `gc_set_threshold` functions.
NUM_THREADS = 8
NUM_ITERS = 100_000
barrier = threading.Barrier(NUM_THREADS)
class CyclicReference:
def __init__(self):
self.r = self
def allocator():
barrier.wait()
for _ in range(NUM_ITERS):
CyclicReference()
def setter():
barrier.wait()
for i in range(NUM_ITERS):
gc.set_threshold(100 + (i % 100), 10 + (i % 10), 10 + (i % 10))
current_threshold = gc.get_threshold()
try:
threads = [Thread(target=allocator) for _ in range(NUM_THREADS - 1)]
threads.append(Thread(target=setter))
with threading_helper.start_threads(threads):
pass
finally:
gc.set_threshold(*current_threshold)
if __name__ == "__main__":
unittest.main()