Skip to content

Commit a0ecaab

Browse files
authored
[3.5] bpo-24484: Avoid race condition in multiprocessing cleanup (GH-2159) (#2167)
* 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 d071a20 commit a0ecaab

File tree

3 files changed

+86
-13
lines changed

3 files changed

+86
-13
lines changed

Lib/multiprocessing/util.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -241,20 +241,28 @@ def _run_finalizers(minpriority=None):
241241
return
242242

243243
if minpriority is None:
244-
f = lambda p : p[0][0] is not None
244+
f = lambda p : p[0] is not None
245245
else:
246-
f = lambda p : p[0][0] is not None and p[0][0] >= minpriority
247-
248-
items = [x for x in list(_finalizer_registry.items()) if f(x)]
249-
items.sort(reverse=True)
250-
251-
for key, finalizer in items:
252-
sub_debug('calling %s', finalizer)
253-
try:
254-
finalizer()
255-
except Exception:
256-
import traceback
257-
traceback.print_exc()
246+
f = lambda p : p[0] is not None and p[0] >= minpriority
247+
248+
# Careful: _finalizer_registry may be mutated while this function
249+
# is running (either by a GC run or by another thread).
250+
251+
# list(_finalizer_registry) should be atomic, while
252+
# list(_finalizer_registry.items()) is not.
253+
keys = [key for key in list(_finalizer_registry) if f(key)]
254+
keys.sort(reverse=True)
255+
256+
for key in keys:
257+
finalizer = _finalizer_registry.get(key)
258+
# key may have been removed from the registry
259+
if finalizer is not None:
260+
sub_debug('calling %s', finalizer)
261+
try:
262+
finalizer()
263+
except Exception:
264+
import traceback
265+
traceback.print_exc()
258266

259267
if minpriority is None:
260268
_finalizer_registry.clear()

Lib/test/_test_multiprocessing.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2985,6 +2985,14 @@ class _TestFinalize(BaseTestCase):
29852985

29862986
ALLOWED_TYPES = ('processes',)
29872987

2988+
def setUp(self):
2989+
self.registry_backup = util._finalizer_registry.copy()
2990+
util._finalizer_registry.clear()
2991+
2992+
def tearDown(self):
2993+
self.assertFalse(util._finalizer_registry)
2994+
util._finalizer_registry.update(self.registry_backup)
2995+
29882996
@classmethod
29892997
def _test_finalize(cls, conn):
29902998
class Foo(object):
@@ -3034,6 +3042,61 @@ def test_finalize(self):
30343042
result = [obj for obj in iter(conn.recv, 'STOP')]
30353043
self.assertEqual(result, ['a', 'b', 'd10', 'd03', 'd02', 'd01', 'e'])
30363044

3045+
def test_thread_safety(self):
3046+
# bpo-24484: _run_finalizers() should be thread-safe
3047+
def cb():
3048+
pass
3049+
3050+
class Foo(object):
3051+
def __init__(self):
3052+
self.ref = self # create reference cycle
3053+
# insert finalizer at random key
3054+
util.Finalize(self, cb, exitpriority=random.randint(1, 100))
3055+
3056+
finish = False
3057+
exc = None
3058+
3059+
def run_finalizers():
3060+
nonlocal exc
3061+
while not finish:
3062+
time.sleep(random.random() * 1e-1)
3063+
try:
3064+
# A GC run will eventually happen during this,
3065+
# collecting stale Foo's and mutating the registry
3066+
util._run_finalizers()
3067+
except Exception as e:
3068+
exc = e
3069+
3070+
def make_finalizers():
3071+
nonlocal exc
3072+
d = {}
3073+
while not finish:
3074+
try:
3075+
# Old Foo's get gradually replaced and later
3076+
# collected by the GC (because of the cyclic ref)
3077+
d[random.getrandbits(5)] = {Foo() for i in range(10)}
3078+
except Exception as e:
3079+
exc = e
3080+
d.clear()
3081+
3082+
old_interval = sys.getswitchinterval()
3083+
old_threshold = gc.get_threshold()
3084+
try:
3085+
sys.setswitchinterval(1e-6)
3086+
gc.set_threshold(5, 5, 5)
3087+
threads = [threading.Thread(target=run_finalizers),
3088+
threading.Thread(target=make_finalizers)]
3089+
with test.support.start_threads(threads):
3090+
time.sleep(4.0) # Wait a bit to trigger race condition
3091+
finish = True
3092+
if exc is not None:
3093+
raise exc
3094+
finally:
3095+
sys.setswitchinterval(old_interval)
3096+
gc.set_threshold(*old_threshold)
3097+
gc.collect() # Collect remaining Foo's
3098+
3099+
30373100
#
30383101
# Test that from ... import * works for each module
30393102
#

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Extension Modules
5656
Library
5757
-------
5858

59+
- bpo-24484: Avoid race condition in multiprocessing cleanup.
60+
5961
- bpo-28994: The traceback no longer displayed for SystemExit raised in
6062
a callback registered by atexit.
6163

0 commit comments

Comments
 (0)