# Free-threaded build: iterating an instance __dict__ (split table) with # .values()/.items() while another thread deletes attributes can segfault. # Each reader thread creates its own iterator; no iterator is shared. # Exit status: 0 = no crash; 3 = a worker raised or did no work (run invalid); # 4 = GIL enabled, nothing was tested. import sys, threading, time, traceback if getattr(sys, "_is_gil_enabled", lambda: True)(): sys.stderr.write("SKIP: needs a free-threaded build with the GIL disabled " "(sys._is_gil_enabled() is not False). Nothing tested.\n") sys.exit(4) DURATION = float(sys.argv[1]) if len(sys.argv) > 1 else 10.0 class C: pass o = C() for i in range(8): setattr(o, f"a{i}", i) d = o.__dict__ # split-table dict sharing o's inline values stop = threading.Event() counts = [0, 0, 0] # passes: mutator, reader 1, reader 2 errors = [] def mutator(): while not stop.is_set(): for i in range(8): delattr(o, f"a{i}") # stores NULL into the values slot first setattr(o, f"a{i}", i) counts[0] += 1 def reader(k): while not stop.is_set(): try: for v in d.values(): # each thread uses its own iterator pass except RuntimeError: # "changed size during iteration" is fine pass counts[k] += 1 def guarded(fn, *args): # a worker exception fails the run try: fn(*args) except BaseException: errors.append(traceback.format_exc()) stop.set() ts = [threading.Thread(target=guarded, args=(mutator,))] + \ [threading.Thread(target=guarded, args=(reader, k)) for k in (1, 2)] for t in ts: t.start() time.sleep(DURATION) stop.set() for t in ts: t.join() if errors or 0 in counts: sys.stderr.write("".join(errors)) print(f"INVALID: {len(errors)} worker exception(s), passes={counts}") sys.exit(3) print(f"OK: no crash (passes: mutator={counts[0]}, readers={counts[1:]})")