Skip to content

Commit fa4f0a1

Browse files
authored
gh-90622: Prevent max_tasks_per_child use with a fork mp_context. (#91587)
Prevent `max_tasks_per_child` use with a "fork" mp_context to avoid deadlocks. Also defaults to "spawn" when no mp_context is supplied for safe convenience.
1 parent 2b563f1 commit fa4f0a1

File tree

4 files changed

+43
-11
lines changed

4 files changed

+43
-11
lines changed

Doc/library/concurrent.futures.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,11 @@ to a :class:`ProcessPoolExecutor` will result in deadlock.
254254

255255
*max_tasks_per_child* is an optional argument that specifies the maximum
256256
number of tasks a single process can execute before it will exit and be
257-
replaced with a fresh worker process. The default *max_tasks_per_child* is
258-
``None`` which means worker processes will live as long as the pool.
257+
replaced with a fresh worker process. By default *max_tasks_per_child* is
258+
``None`` which means worker processes will live as long as the pool. When
259+
a max is specified, the "spawn" multiprocessing start method will be used by
260+
default in absense of a *mp_context* parameter. This feature is incompatible
261+
with the "fork" start method.
259262

260263
.. versionchanged:: 3.3
261264
When one of the worker processes terminates abruptly, a

Lib/concurrent/futures/process.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -617,14 +617,16 @@ def __init__(self, max_workers=None, mp_context=None,
617617
execute the given calls. If None or not given then as many
618618
worker processes will be created as the machine has processors.
619619
mp_context: A multiprocessing context to launch the workers. This
620-
object should provide SimpleQueue, Queue and Process.
620+
object should provide SimpleQueue, Queue and Process. Useful
621+
to allow specific multiprocessing start methods.
621622
initializer: A callable used to initialize worker processes.
622623
initargs: A tuple of arguments to pass to the initializer.
623-
max_tasks_per_child: The maximum number of tasks a worker process can
624-
complete before it will exit and be replaced with a fresh
625-
worker process, to enable unused resources to be freed. The
626-
default value is None, which means worker process will live
627-
as long as the executor will live.
624+
max_tasks_per_child: The maximum number of tasks a worker process
625+
can complete before it will exit and be replaced with a fresh
626+
worker process. The default of None means worker process will
627+
live as long as the executor. Requires a non-'fork' mp_context
628+
start method. When given, we default to using 'spawn' if no
629+
mp_context is supplied.
628630
"""
629631
_check_system_limits()
630632

@@ -644,7 +646,10 @@ def __init__(self, max_workers=None, mp_context=None,
644646
self._max_workers = max_workers
645647

646648
if mp_context is None:
647-
mp_context = mp.get_context()
649+
if max_tasks_per_child is not None:
650+
mp_context = mp.get_context("spawn")
651+
else:
652+
mp_context = mp.get_context()
648653
self._mp_context = mp_context
649654

650655
if initializer is not None and not callable(initializer):
@@ -657,6 +662,11 @@ def __init__(self, max_workers=None, mp_context=None,
657662
raise TypeError("max_tasks_per_child must be an integer")
658663
elif max_tasks_per_child <= 0:
659664
raise ValueError("max_tasks_per_child must be >= 1")
665+
if self._mp_context.get_start_method(allow_none=False) == "fork":
666+
# https://github.com/python/cpython/issues/90622
667+
raise ValueError("max_tasks_per_child is incompatible with"
668+
" the 'fork' multiprocessing start method;"
669+
" supply a different mp_context.")
660670
self._max_tasks_per_child = max_tasks_per_child
661671

662672
# Management thread

Lib/test/test_concurrent_futures.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -1039,10 +1039,15 @@ def test_idle_process_reuse_multiple(self):
10391039
executor.shutdown()
10401040

10411041
def test_max_tasks_per_child(self):
1042+
context = self.get_context()
1043+
if context.get_start_method(allow_none=False) == "fork":
1044+
with self.assertRaises(ValueError):
1045+
self.executor_type(1, mp_context=context, max_tasks_per_child=3)
1046+
return
10421047
# not using self.executor as we need to control construction.
10431048
# arguably this could go in another class w/o that mixin.
10441049
executor = self.executor_type(
1045-
1, mp_context=self.get_context(), max_tasks_per_child=3)
1050+
1, mp_context=context, max_tasks_per_child=3)
10461051
f1 = executor.submit(os.getpid)
10471052
original_pid = f1.result()
10481053
# The worker pid remains the same as the worker could be reused
@@ -1061,11 +1066,20 @@ def test_max_tasks_per_child(self):
10611066

10621067
executor.shutdown()
10631068

1069+
def test_max_tasks_per_child_defaults_to_spawn_context(self):
1070+
# not using self.executor as we need to control construction.
1071+
# arguably this could go in another class w/o that mixin.
1072+
executor = self.executor_type(1, max_tasks_per_child=3)
1073+
self.assertEqual(executor._mp_context.get_start_method(), "spawn")
1074+
10641075
def test_max_tasks_early_shutdown(self):
1076+
context = self.get_context()
1077+
if context.get_start_method(allow_none=False) == "fork":
1078+
raise unittest.SkipTest("Incompatible with the fork start method.")
10651079
# not using self.executor as we need to control construction.
10661080
# arguably this could go in another class w/o that mixin.
10671081
executor = self.executor_type(
1068-
3, mp_context=self.get_context(), max_tasks_per_child=1)
1082+
3, mp_context=context, max_tasks_per_child=1)
10691083
futures = []
10701084
for i in range(6):
10711085
futures.append(executor.submit(mul, i, i))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
In ``concurrent.futures.process.ProcessPoolExecutor`` disallow the "fork"
2+
multiprocessing start method when the new ``max_tasks_per_child`` feature is
3+
used as the mix of threads+fork can hang the child processes. Default to
4+
using the safe "spawn" start method in that circumstance if no
5+
``mp_context`` was supplied.

0 commit comments

Comments
 (0)