Skip to content

Commit d09f167

Browse files
authored
[2.7] bpo-24484: Avoid race condition in multiprocessing cleanup (GH-2159) (#2168)
* bpo-24484: Avoid race condition in multiprocessing cleanup The finalizer registry can be mutated while inspected by multiprocessing at process exit. * Use test.support.start_threads() * Add Misc/NEWS. (cherry picked from commit 1eb6c00)
1 parent a51f12f commit d09f167

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

‎Lib/multiprocessing/util.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ def _run_finalizers(minpriority=None):
265265
else:
266266
f = lambda p : p[0][0] is not None and p[0][0] >= minpriority
267267

268+
# Careful: _finalizer_registry may be mutated while this function
269+
# is running (either by a GC run or by another thread).
270+
268271
items = [x for x in _finalizer_registry.items() if f(x)]
269272
items.sort(reverse=True)
270273

‎Lib/test/test_multiprocessing.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,6 +2088,14 @@ class _TestFinalize(BaseTestCase):
20882088

20892089
ALLOWED_TYPES = ('processes',)
20902090

2091+
def setUp(self):
2092+
self.registry_backup = util._finalizer_registry.copy()
2093+
util._finalizer_registry.clear()
2094+
2095+
def tearDown(self):
2096+
self.assertFalse(util._finalizer_registry)
2097+
util._finalizer_registry.update(self.registry_backup)
2098+
20912099
@classmethod
20922100
def _test_finalize(cls, conn):
20932101
class Foo(object):
@@ -2137,6 +2145,59 @@ def test_finalize(self):
21372145
result = [obj for obj in iter(conn.recv, 'STOP')]
21382146
self.assertEqual(result, ['a', 'b', 'd10', 'd03', 'd02', 'd01', 'e'])
21392147

2148+
def test_thread_safety(self):
2149+
# bpo-24484: _run_finalizers() should be thread-safe
2150+
def cb():
2151+
pass
2152+
2153+
class Foo(object):
2154+
def __init__(self):
2155+
self.ref = self # create reference cycle
2156+
# insert finalizer at random key
2157+
util.Finalize(self, cb, exitpriority=random.randint(1, 100))
2158+
2159+
finish = False
2160+
exc = []
2161+
2162+
def run_finalizers():
2163+
while not finish:
2164+
time.sleep(random.random() * 1e-1)
2165+
try:
2166+
# A GC run will eventually happen during this,
2167+
# collecting stale Foo's and mutating the registry
2168+
util._run_finalizers()
2169+
except Exception as e:
2170+
exc.append(e)
2171+
2172+
def make_finalizers():
2173+
d = {}
2174+
while not finish:
2175+
try:
2176+
# Old Foo's get gradually replaced and later
2177+
# collected by the GC (because of the cyclic ref)
2178+
d[random.getrandbits(5)] = {Foo() for i in range(10)}
2179+
except Exception as e:
2180+
exc.append(e)
2181+
d.clear()
2182+
2183+
old_interval = sys.getcheckinterval()
2184+
old_threshold = gc.get_threshold()
2185+
try:
2186+
sys.setcheckinterval(10)
2187+
gc.set_threshold(5, 5, 5)
2188+
threads = [threading.Thread(target=run_finalizers),
2189+
threading.Thread(target=make_finalizers)]
2190+
with test_support.start_threads(threads):
2191+
time.sleep(4.0) # Wait a bit to trigger race condition
2192+
finish = True
2193+
if exc:
2194+
raise exc[0]
2195+
finally:
2196+
sys.setcheckinterval(old_interval)
2197+
gc.set_threshold(*old_threshold)
2198+
gc.collect() # Collect remaining Foo's
2199+
2200+
21402201
#
21412202
# Test that from ... import * works for each module
21422203
#

0 commit comments

Comments
 (0)