Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Lib/multiprocessing/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ def _run_finalizers(minpriority=None):
else:
f = lambda p : p[0][0] is not None and p[0][0] >= minpriority

# Careful: _finalizer_registry may be mutated while this function
# is running (either by a GC run or by another thread).

items = [x for x in _finalizer_registry.items() if f(x)]
items.sort(reverse=True)

Expand Down
61 changes: 61 additions & 0 deletions Lib/test/test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,14 @@ class _TestFinalize(BaseTestCase):

ALLOWED_TYPES = ('processes',)

def setUp(self):
self.registry_backup = util._finalizer_registry.copy()
util._finalizer_registry.clear()

def tearDown(self):
self.assertFalse(util._finalizer_registry)
util._finalizer_registry.update(self.registry_backup)

@classmethod
def _test_finalize(cls, conn):
class Foo(object):
Expand Down Expand Up @@ -2137,6 +2145,59 @@ def test_finalize(self):
result = [obj for obj in iter(conn.recv, 'STOP')]
self.assertEqual(result, ['a', 'b', 'd10', 'd03', 'd02', 'd01', 'e'])

def test_thread_safety(self):
# bpo-24484: _run_finalizers() should be thread-safe
def cb():
pass

class Foo(object):
def __init__(self):
self.ref = self # create reference cycle
# insert finalizer at random key
util.Finalize(self, cb, exitpriority=random.randint(1, 100))

finish = False
exc = []

def run_finalizers():
while not finish:
time.sleep(random.random() * 1e-1)
try:
# A GC run will eventually happen during this,
# collecting stale Foo's and mutating the registry
util._run_finalizers()
except Exception as e:
exc.append(e)

def make_finalizers():
d = {}
while not finish:
try:
# Old Foo's get gradually replaced and later
# collected by the GC (because of the cyclic ref)
d[random.getrandbits(5)] = {Foo() for i in range(10)}
except Exception as e:
exc.append(e)
d.clear()

old_interval = sys.getcheckinterval()
old_threshold = gc.get_threshold()
try:
sys.setcheckinterval(10)
gc.set_threshold(5, 5, 5)
threads = [threading.Thread(target=run_finalizers),
threading.Thread(target=make_finalizers)]
with test_support.start_threads(threads):
time.sleep(4.0) # Wait a bit to trigger race condition
finish = True
if exc:
raise exc[0]
finally:
sys.setcheckinterval(old_interval)
gc.set_threshold(*old_threshold)
gc.collect() # Collect remaining Foo's


#
# Test that from ... import * works for each module
#
Expand Down