Skip to content

Commit ac10e0c

Browse files
bpo-39360: Ensure all workers exit when finalizing a multiprocessing Pool (GH-19009)
When the pull is not used via the context manager or terminate() is called, there is a system in multiprocessing.util that handles finalization of all pools via an atexit handler (the Finalize) class. This class registers the _terminate_pool handler in the registry of finalizers of the module, and that registry is called on interpreter exit via _exit_function. The problem is that the "happy" path with the context manager or manual call to finalize() does some extra steps that _terminate_pool does not. The step that is not executed when the atexit() handler calls _terminate_pool is pinging the _change_notifier queue to unblock the maintenance threads. This commit moves the notification to the _terminate_pool function so is called from both code paths. Co-authored-by: Pablo Galindo <[email protected]>
1 parent c81609e commit ac10e0c

File tree

3 files changed

+27
-2
lines changed

3 files changed

+27
-2
lines changed

Lib/multiprocessing/pool.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,8 +651,6 @@ def close(self):
651651
def terminate(self):
652652
util.debug('terminating pool')
653653
self._state = TERMINATE
654-
self._worker_handler._state = TERMINATE
655-
self._change_notifier.put(None)
656654
self._terminate()
657655

658656
def join(self):
@@ -682,7 +680,12 @@ def _terminate_pool(cls, taskqueue, inqueue, outqueue, pool, change_notifier,
682680
# this is guaranteed to only be called once
683681
util.debug('finalizing pool')
684682

683+
# Notify that the worker_handler state has been changed so the
684+
# _handle_workers loop can be unblocked (and exited) in order to
685+
# send the finalization sentinel all the workers.
685686
worker_handler._state = TERMINATE
687+
change_notifier.put(None)
688+
686689
task_handler._state = TERMINATE
687690

688691
util.debug('helping task handler/workers to finish')

Lib/test/_test_multiprocessing.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,6 +2780,24 @@ def test_pool_worker_lifetime_early_close(self):
27802780
for (j, res) in enumerate(results):
27812781
self.assertEqual(res.get(), sqr(j))
27822782

2783+
def test_worker_finalization_via_atexit_handler_of_multiprocessing(self):
2784+
# tests cases against bpo-38744 and bpo-39360
2785+
cmd = '''if 1:
2786+
from multiprocessing import Pool
2787+
problem = None
2788+
class A:
2789+
def __init__(self):
2790+
self.pool = Pool(processes=1)
2791+
def test():
2792+
global problem
2793+
problem = A()
2794+
problem.pool.map(float, tuple(range(10)))
2795+
if __name__ == "__main__":
2796+
test()
2797+
'''
2798+
rc, out, err = test.support.script_helper.assert_python_ok('-c', cmd)
2799+
self.assertEqual(rc, 0)
2800+
27832801
#
27842802
# Test of creating a customized manager class
27852803
#
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Ensure all workers exit when finalizing a :class:`multiprocessing.Pool` implicitly via the module finalization
2+
handlers of multiprocessing. This fixes a deadlock situation that can be experienced when the Pool is not
3+
properly finalized via the context manager or a call to ``multiprocessing.Pool.terminate``. Patch by Batuhan Taskaya
4+
and Pablo Galindo.

0 commit comments

Comments
 (0)