Skip to content
Next Next commit
gh-90622: Do not spawn ProcessPool workers on demand when they spawn …
…via fork.

This avoids potential deadlocks in the child processes due to forking from
a multithreaded process.
  • Loading branch information
gpshead committed Apr 16, 2022
commit e7e895f2396ca8d34207dde804b232843801e759
39 changes: 29 additions & 10 deletions Lib/concurrent/futures/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,10 @@ def __init__(self, max_workers=None, mp_context=None,
mp_context = mp.get_context()
self._mp_context = mp_context

# https://github.com/python/cpython/issues/90622
self._safe_to_dynamically_spawn_children = (
self._mp_context.get_start_method(allow_none=False) != "fork")

if initializer is not None and not callable(initializer):
raise TypeError("initializer must be a callable")
self._initializer = initializer
Expand Down Expand Up @@ -701,6 +705,8 @@ def __init__(self, max_workers=None, mp_context=None,
def _start_executor_manager_thread(self):
if self._executor_manager_thread is None:
# Start the processes so that their sentinels are known.
if not self._safe_to_dynamically_spawn_children: # ie, using fork.
self.__launch_processes()
self._executor_manager_thread = _ExecutorManagerThread(self)
self._executor_manager_thread.start()
_threads_wakeups[self._executor_manager_thread] = \
Expand All @@ -713,15 +719,27 @@ def _adjust_process_count(self):

process_count = len(self._processes)
if process_count < self._max_workers:
p = self._mp_context.Process(
target=_process_worker,
args=(self._call_queue,
self._result_queue,
self._initializer,
self._initargs,
self._max_tasks_per_child))
p.start()
self._processes[p.pid] = p
assert self._safe_to_dynamically_spawn_children or not self._executor_manager_thread, 'https://github.com/python/cpython/issues/90622'
Comment thread
gpshead marked this conversation as resolved.
Outdated
self.__spawn_process()

def __launch_processes(self):
Comment thread
gpshead marked this conversation as resolved.
Outdated
# https://github.com/python/cpython/issues/90622
assert not self._executor_manager_thread, (
'Processes cannot be fork()ed after the thread has started, '
'deadlock in the child processes could result.')
for _ in range(len(self._processes), self._max_workers):
self.__spawn_process()

def __spawn_process(self):
p = self._mp_context.Process(
target=_process_worker,
args=(self._call_queue,
self._result_queue,
self._initializer,
self._initargs,
self._max_tasks_per_child))
p.start()
self._processes[p.pid] = p

def submit(self, fn, /, *args, **kwargs):
with self._shutdown_lock:
Expand All @@ -742,7 +760,8 @@ def submit(self, fn, /, *args, **kwargs):
# Wake up queue management thread
self._executor_manager_thread_wakeup.wakeup()

self._adjust_process_count()
if self._safe_to_dynamically_spawn_children:
self._adjust_process_count()
self._start_executor_manager_thread()
return f
submit.__doc__ = _base.Executor.submit.__doc__
Expand Down
9 changes: 7 additions & 2 deletions Lib/test/test_concurrent_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,16 @@ def test_processes_terminate(self):
def acquire_lock(lock):
lock.acquire()

mp_context = get_context()
mp_context = self.get_context()
if mp_context.get_start_method(allow_none=False) == "fork":
# fork pre-spawns, not on demand.
expected_num_processes = self.worker_count
else:
expected_num_processes = 3
sem = mp_context.Semaphore(0)
for _ in range(3):
self.executor.submit(acquire_lock, sem)
self.assertEqual(len(self.executor._processes), 3)
self.assertEqual(len(self.executor._processes), expected_num_processes)
for _ in range(3):
sem.release()
processes = self.executor._processes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Worker processes for :class:`concurrent.futures.ProcessPoolExecutor` are no
longer spawned on demand (a feature added in 3.9) when the multiprocessing
context start method is ``"fork"`` as that can lead to deadlocks in the
child processes due to a fork happening while threads are running.