| import unittest | |
| import threading | |
| from threading import Thread | |
| import time | |
| from unittest import TestCase | |
| import gc | |
| import weakref | |
| from test import support | |
| 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_merge_brc_queue_of_detached_thread(self): | |
| # GH-157838: objects queued for merging by a thread that is detached | |
| # (blocked in a lock acquire, sleep, etc.) are merged and freed on its | |
| # behalf instead of staying alive until it runs Python code again. | |
| lock = threading.Lock() | |
| lock.acquire() | |
| ready = threading.Event() | |
| objs = [] | |
| def worker(): | |
| # Objects owned by this thread; only the list holds a reference. | |
| objs.extend(MyObj() for _ in range(100)) | |
| ready.set() | |
| lock.acquire() # block while detached | |
| thread = Thread(target=worker) | |
| thread.start() | |
| try: | |
| ready.wait() | |
| # The worker may not have detached yet when the first objects | |
| # are dropped; keep trying until one is freed immediately. | |
| for _ in support.sleeping_retry(support.SHORT_TIMEOUT, error=False): | |
| obj = objs.pop() | |
| wr = weakref.ref(obj) | |
| del obj | |
| if wr() is None: | |
| break | |
| else: | |
| self.fail("object not freed while owning thread was detached") | |
| finally: | |
| lock.release() | |
| thread.join() | |
| def test_gc_callbacks_race_with_mutation(self): | |
| def collect(): | |
| b.wait() | |
| while not stop.is_set(): | |
| gc.collect() | |
| def mutate(): | |
| b.wait() | |
| while not stop.is_set(): | |
| gc.callbacks[:] = [lambda *_: _ for _ in range(16)] | |
| time.sleep(0) | |
| gc.callbacks.clear() | |
| threads = [threading.Thread(target=f) for f in (collect, mutate) * 4] | |
| b = threading.Barrier(len(threads) + 1) | |
| stop = threading.Event() | |
| with threading_helper.start_threads(threads, stop.set): | |
| b.wait() | |
| time.sleep(0.2) | |
| 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) | |
| def test_get_count(self): | |
| class CyclicReference: | |
| def __init__(self): | |
| self.ref = self | |
| NUM_ALLOCATORS = 7 | |
| NUM_READERS = 1 | |
| NUM_THREADS = NUM_ALLOCATORS + NUM_READERS | |
| NUM_ITERS = 1000 | |
| barrier = threading.Barrier(NUM_THREADS) | |
| def allocator(): | |
| barrier.wait() | |
| for _ in range(NUM_ITERS): | |
| CyclicReference() | |
| def reader(): | |
| barrier.wait() | |
| for _ in range(NUM_ITERS): | |
| gc.get_count() | |
| threads = [Thread(target=allocator) for _ in range(NUM_ALLOCATORS)] | |
| threads.extend(Thread(target=reader) for _ in range(NUM_READERS)) | |
| with threading_helper.start_threads(threads): | |
| pass | |
| if __name__ == "__main__": | |
| unittest.main() |