Visitar URL original
GH-84559: Deprecate fork being the multiprocessing default. by gpshead · Pull Request #100618 · python/cpython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
GH-84559: Deprecate fork being the multiprocessing default.
This starts the process. Users who don't specify their own start method
and use the default on platforms where it is 'fork' will see a
DeprecationWarning upon multiprocessing.Pool() construction or upon
multiprocessing.Process.start().
  • Loading branch information
gpshead committed Dec 30, 2022
commit 1cecbdb8824f552ddc8399967aff667f0150beea
34 changes: 33 additions & 1 deletion Lib/multiprocessing/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,32 @@ def _Popen(process_obj):
from .popen_fork import Popen
return Popen(process_obj)

class _DeprecatedForkProcess(ForkProcess):
@classmethod
def _warn(cls, stacklevel):
import warnings
warnings.warn(
"Use of the multiprocessing 'fork' start method by default is "
"deprecated. "
"The default will change in Python >= 3.14, per GH-84559. "
"Please use an explicit multiprocessing.get_context or "
"multiprocessing.set_start_method API call to specify your "
"application's start method if you want to use 'fork'."
" The 'spawn' and 'forkserver' start methods are safer "
"depending on the platform and application.",
category=DeprecationWarning,
stacklevel=stacklevel,
)

# Blame the process.start() method call.
_ORIG_WARNING_STACKLEVEL = 5
_warning_stacklevel = _ORIG_WARNING_STACKLEVEL

@classmethod
def _Popen(cls, process_obj):
cls._warn(stacklevel=cls._warning_stacklevel)
return super()._Popen(process_obj)

class SpawnProcess(process.BaseProcess):
_start_method = 'spawn'
@staticmethod
Expand All @@ -303,6 +329,9 @@ class ForkContext(BaseContext):
_name = 'fork'
Process = ForkProcess

class _DefaultForkContext(ForkContext):
Process = _DeprecatedForkProcess

class SpawnContext(BaseContext):
_name = 'spawn'
Process = SpawnProcess
Expand All @@ -318,13 +347,16 @@ def _check_available(self):
'fork': ForkContext(),
'spawn': SpawnContext(),
'forkserver': ForkServerContext(),
# Remove None and _DefaultForkContext() when changing the default
# in 3.14 for https://github.com/python/cpython/issues/84559.
None: _DefaultForkContext(),
}
if sys.platform == 'darwin':
# bpo-33725: running arbitrary code after fork() is no longer reliable
# on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
_default_context = DefaultContext(_concrete_contexts['spawn'])
else:
_default_context = DefaultContext(_concrete_contexts['fork'])
_default_context = DefaultContext(_concrete_contexts[None])

else:

Expand Down
13 changes: 13 additions & 0 deletions Lib/multiprocessing/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,20 @@ def _repopulate_pool_static(ctx, Process, processes, pool, inqueue,
wrap_exception))
w.name = w.name.replace('Process', 'PoolWorker')
w.daemon = True
if _warn := getattr(w, '_warn', None): # GH-84559
# Hacky, but BaseProcess._Popen does everything via the class
# using static or class methods with no instance. The way
# multiprocessing APIs get used, a concurrency mess here
# with a Pool() being constructed at the same time as other
# threads are calling Process.start() is highly unlikely.
# If so: The oddball application gets the warning attributed
# to an unusual spot in the stack on occasion.
process_class = w.__class__
process_class._warning_stacklevel = 8 # blame mp.Pool()
w.start()
if _warn:
process_class._warning_stacklevel = (
process_class._ORIG_WARNING_STACKLEVEL)
pool.append(w)
util.debug('added worker')

Expand Down
63 changes: 63 additions & 0 deletions Lib/test/test_multiprocessing_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Test default behavior of multiprocessing."""

from inspect import currentframe, getframeinfo
import multiprocessing
import sys
from test.support import threading_helper
import unittest
import warnings


def do_nothing():
pass


# Process has the same API as Thread so this helper works.
join_process = threading_helper.join_thread


class DefaultWarningsTest(unittest.TestCase):

@unittest.skipIf(sys.platform in ('win32', 'darwin'),
'The default is not "fork" on Windows or macOS.')
def setUp(self):
self.assertEqual(multiprocessing.get_start_method(), 'fork')
self.assertIsInstance(multiprocessing.get_context(),
multiprocessing.context._DefaultForkContext)

def test_default_fork_start_method_warning_process(self):
with warnings.catch_warnings(record=True) as ws:
warnings.simplefilter('always')
process = multiprocessing.Process(target=do_nothing)
process.start() # warning should point here.
join_process(process)
self.assertEqual(len(ws), 1, msg=[str(x) for x in ws])
self.assertIn(__file__, ws[0].filename)
self.assertEqual(getframeinfo(currentframe()).lineno-4, ws[0].lineno)
self.assertIn("'fork'", str(ws[0].message))
self.assertIn("start_method API", str(ws[0].message))

def test_default_fork_start_method_warning_pool(self):
with warnings.catch_warnings(record=True) as ws:
warnings.simplefilter('always')
pool = multiprocessing.Pool(1) # warning should point here.
pool.terminate()
pool.join()
self.assertEqual(len(ws), 1, msg=[str(x) for x in ws])
self.assertIn(__file__, ws[0].filename)
self.assertEqual(getframeinfo(currentframe()).lineno-5, ws[0].lineno)
self.assertIn("'fork'", str(ws[0].message))
self.assertIn("start_method API", str(ws[0].message))

def test_no_warning_when_using_explicit_fork_mp_context(self):
with warnings.catch_warnings(record=True) as ws:
warnings.simplefilter('always') # Enable all warnings.
fork_mp = multiprocessing.get_context('fork')
pool = fork_mp.Pool(1)
pool.terminate()
pool.join()
self.assertEqual(len(ws), 0, msg=[str(x) for x in ws])


if __name__ == '__main__':
unittest.main()