Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6fd0345

Browse files
authored
[3.6] bpo-24484: Avoid race condition in multiprocessing cleanup (GH-2159) (#2166)
* 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 2bfb45d commit 6fd0345

3 files changed

Lines changed: 86 additions & 13 deletions

File tree

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
@@ -3075,6 +3075,14 @@ class _TestFinalize(BaseTestCase):
30753075

30763076
ALLOWED_TYPES = ('processes',)
30773077

3078+
def setUp(self):
3079+
self.registry_backup = util._finalizer_registry.copy()
3080+
util._finalizer_registry.clear()
3081+
3082+
def tearDown(self):
3083+
self.assertFalse(util._finalizer_registry)
3084+
util._finalizer_registry.update(self.registry_backup)
3085+
30783086
@classmethod
30793087
def _test_finalize(cls, conn):
30803088
class Foo(object):
@@ -3124,6 +3132,61 @@ def test_finalize(self):
31243132
result = [obj for obj in iter(conn.recv, 'STOP')]
31253133
self.assertEqual(result, ['a', 'b', 'd10', 'd03', 'd02', 'd01', 'e'])
31263134

3135+
def test_thread_safety(self):
3136+
# bpo-24484: _run_finalizers() should be thread-safe
3137+
def cb():
3138+
pass
3139+
3140+
class Foo(object):
3141+
def __init__(self):
3142+
self.ref = self # create reference cycle
3143+
# insert finalizer at random key
3144+
util.Finalize(self, cb, exitpriority=random.randint(1, 100))
3145+
3146+
finish = False
3147+
exc = None
3148+
3149+
def run_finalizers():
3150+
nonlocal exc
3151+
while not finish:
3152+
time.sleep(random.random() * 1e-1)
3153+
try:
3154+
# A GC run will eventually happen during this,
3155+
# collecting stale Foo's and mutating the registry
3156+
util._run_finalizers()
3157+
except Exception as e:
3158+
exc = e
3159+
3160+
def make_finalizers():
3161+
nonlocal exc
3162+
d = {}
3163+
while not finish:
3164+
try:
3165+
# Old Foo's get gradually replaced and later
3166+
# collected by the GC (because of the cyclic ref)
3167+
d[random.getrandbits(5)] = {Foo() for i in range(10)}
3168+
except Exception as e:
3169+
exc = e
3170+
d.clear()
3171+
3172+
old_interval = sys.getswitchinterval()
3173+
old_threshold = gc.get_threshold()
3174+
try:
3175+
sys.setswitchinterval(1e-6)
3176+
gc.set_threshold(5, 5, 5)
3177+
threads = [threading.Thread(target=run_finalizers),
3178+
threading.Thread(target=make_finalizers)]
3179+
with test.support.start_threads(threads):
3180+
time.sleep(4.0) # Wait a bit to trigger race condition
3181+
finish = True
3182+
if exc is not None:
3183+
raise exc
3184+
finally:
3185+
sys.setswitchinterval(old_interval)
3186+
gc.set_threshold(*old_threshold)
3187+
gc.collect() # Collect remaining Foo's
3188+
3189+
31273190
#
31283191
# Test that from ... import * works for each module
31293192
#

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Core and Builtins
5151
Library
5252
-------
5353

54+
- bpo-24484: Avoid race condition in multiprocessing cleanup (#2159)
55+
5456
- bpo-28994: The traceback no longer displayed for SystemExit raised in
5557
a callback registered by atexit.
5658

0 commit comments

Comments
 (0)