diff options
author | Gregory P. Smith <greg@krypto.org> | 2023-02-02 23:50:35 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-02 23:50:35 (GMT) |
commit | 0ca67e6313c11263ecaef7ce182308eeb5aa6814 (patch) | |
tree | ff302df77417456ee4dcf94082b6d942e320b13a /Lib | |
parent | 618b7a8260bb40290d6551f24885931077309590 (diff) | |
download | cpython-0ca67e6313c11263ecaef7ce182308eeb5aa6814.zip cpython-0ca67e6313c11263ecaef7ce182308eeb5aa6814.tar.gz cpython-0ca67e6313c11263ecaef7ce182308eeb5aa6814.tar.bz2 |
GH-84559: Deprecate fork being the multiprocessing default. (#100618)
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() or concurrent.futures.ProcessPool use.
See the related issue and documentation within this change for details.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/compileall.py | 8 | ||||
-rw-r--r-- | Lib/concurrent/futures/process.py | 23 | ||||
-rw-r--r-- | Lib/multiprocessing/context.py | 29 | ||||
-rw-r--r-- | Lib/test/_test_multiprocessing.py | 21 | ||||
-rw-r--r-- | Lib/test/_test_venv_multiprocessing.py | 1 | ||||
-rw-r--r-- | Lib/test/test_asyncio/test_events.py | 9 | ||||
-rw-r--r-- | Lib/test/test_concurrent_futures.py | 19 | ||||
-rw-r--r-- | Lib/test/test_fcntl.py | 8 | ||||
-rw-r--r-- | Lib/test/test_logging.py | 5 | ||||
-rw-r--r-- | Lib/test/test_multiprocessing_defaults.py | 82 | ||||
-rw-r--r-- | Lib/test/test_pickle.py | 2 | ||||
-rw-r--r-- | Lib/test/test_re.py | 3 |
12 files changed, 190 insertions, 20 deletions
diff --git a/Lib/compileall.py b/Lib/compileall.py index a388931..d394156 100644 --- a/Lib/compileall.py +++ b/Lib/compileall.py @@ -97,9 +97,15 @@ def compile_dir(dir, maxlevels=None, ddir=None, force=False, files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels) success = True if workers != 1 and ProcessPoolExecutor is not None: + import multiprocessing + if multiprocessing.get_start_method() == 'fork': + mp_context = multiprocessing.get_context('forkserver') + else: + mp_context = None # If workers == 0, let ProcessPoolExecutor choose workers = workers or None - with ProcessPoolExecutor(max_workers=workers) as executor: + with ProcessPoolExecutor(max_workers=workers, + mp_context=mp_context) as executor: results = executor.map(partial(compile_file, ddir=ddir, force=force, rx=rx, quiet=quiet, diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index 7e2f5fa..257dd02 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -57,6 +57,7 @@ from functools import partial import itertools import sys from traceback import format_exception +import warnings _threads_wakeups = weakref.WeakKeyDictionary() @@ -616,9 +617,9 @@ class ProcessPoolExecutor(_base.Executor): max_workers: The maximum number of processes that can be used to execute the given calls. If None or not given then as many worker processes will be created as the machine has processors. - mp_context: A multiprocessing context to launch the workers. This - object should provide SimpleQueue, Queue and Process. Useful - to allow specific multiprocessing start methods. + mp_context: A multiprocessing context to launch the workers created + using the multiprocessing.get_context('start method') API. This + object should provide SimpleQueue, Queue and Process. initializer: A callable used to initialize worker processes. initargs: A tuple of arguments to pass to the initializer. max_tasks_per_child: The maximum number of tasks a worker process @@ -650,6 +651,22 @@ class ProcessPoolExecutor(_base.Executor): mp_context = mp.get_context("spawn") else: mp_context = mp.get_context() + if (mp_context.get_start_method() == "fork" and + mp_context == mp.context._default_context._default_context): + warnings.warn( + "The default multiprocessing start method will change " + "away from 'fork' in Python >= 3.14, per GH-84559. " + "ProcessPoolExecutor uses multiprocessing. " + "If your application requires the 'fork' multiprocessing " + "start method, explicitly specify that by passing a " + "mp_context= parameter. " + "The safest start method is 'spawn'.", + category=mp.context.DefaultForkDeprecationWarning, + stacklevel=2, + ) + # Avoid the equivalent warning from multiprocessing itself via + # a non-default fork context. + mp_context = mp.get_context("fork") self._mp_context = mp_context # https://github.com/python/cpython/issues/90622 diff --git a/Lib/multiprocessing/context.py b/Lib/multiprocessing/context.py index b1960ea..010a920 100644 --- a/Lib/multiprocessing/context.py +++ b/Lib/multiprocessing/context.py @@ -23,6 +23,9 @@ class TimeoutError(ProcessError): class AuthenticationError(ProcessError): pass +class DefaultForkDeprecationWarning(DeprecationWarning): + pass + # # Base type for contexts. Bound methods of an instance of this type are included in __all__ of __init__.py # @@ -258,6 +261,7 @@ class DefaultContext(BaseContext): return self._actual_context._name def get_all_start_methods(self): + """Returns a list of the supported start methods, default first.""" if sys.platform == 'win32': return ['spawn'] else: @@ -280,6 +284,23 @@ if sys.platform != 'win32': from .popen_fork import Popen return Popen(process_obj) + _warn_package_prefixes = (os.path.dirname(__file__),) + + class _DeprecatedForkProcess(ForkProcess): + @classmethod + def _Popen(cls, process_obj): + import warnings + warnings.warn( + "The default multiprocessing start method will change " + "away from 'fork' in Python >= 3.14, per GH-84559. " + "Use multiprocessing.get_context(X) or .set_start_method(X) to " + "explicitly specify it when your application requires 'fork'. " + "The safest start method is 'spawn'.", + category=DefaultForkDeprecationWarning, + skip_file_prefixes=_warn_package_prefixes, + ) + return super()._Popen(process_obj) + class SpawnProcess(process.BaseProcess): _start_method = 'spawn' @staticmethod @@ -303,6 +324,9 @@ if sys.platform != 'win32': _name = 'fork' Process = ForkProcess + class _DefaultForkContext(ForkContext): + Process = _DeprecatedForkProcess + class SpawnContext(BaseContext): _name = 'spawn' Process = SpawnProcess @@ -318,13 +342,16 @@ if sys.platform != 'win32': '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: diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 2fa75eb..e4a60a4 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -4098,9 +4098,10 @@ class _TestSharedMemory(BaseTestCase): def test_shared_memory_SharedMemoryManager_reuses_resource_tracker(self): # bpo-36867: test that a SharedMemoryManager uses the # same resource_tracker process as its parent. - cmd = '''if 1: + cmd = f'''if 1: from multiprocessing.managers import SharedMemoryManager - + from multiprocessing import set_start_method + set_start_method({multiprocessing.get_start_method()!r}) smm = SharedMemoryManager() smm.start() @@ -4967,11 +4968,13 @@ class TestFlags(unittest.TestCase): conn.send(tuple(sys.flags)) @classmethod - def run_in_child(cls): + def run_in_child(cls, start_method): import json - r, w = multiprocessing.Pipe(duplex=False) - p = multiprocessing.Process(target=cls.run_in_grandchild, args=(w,)) - p.start() + mp = multiprocessing.get_context(start_method) + r, w = mp.Pipe(duplex=False) + p = mp.Process(target=cls.run_in_grandchild, args=(w,)) + with warnings.catch_warnings(category=DeprecationWarning): + p.start() grandchild_flags = r.recv() p.join() r.close() @@ -4982,8 +4985,10 @@ class TestFlags(unittest.TestCase): def test_flags(self): import json # start child process using unusual flags - prog = ('from test._test_multiprocessing import TestFlags; ' + - 'TestFlags.run_in_child()') + prog = ( + 'from test._test_multiprocessing import TestFlags; ' + f'TestFlags.run_in_child({multiprocessing.get_start_method()!r})' + ) data = subprocess.check_output( [sys.executable, '-E', '-S', '-O', '-c', prog]) child_flags, grandchild_flags = json.loads(data.decode('ascii')) diff --git a/Lib/test/_test_venv_multiprocessing.py b/Lib/test/_test_venv_multiprocessing.py index af72e91..044a0c6 100644 --- a/Lib/test/_test_venv_multiprocessing.py +++ b/Lib/test/_test_venv_multiprocessing.py @@ -30,6 +30,7 @@ def test_func(): def main(): + multiprocessing.set_start_method('spawn') test_pool = multiprocessing.Process(target=test_func) test_pool.start() test_pool.join() diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 214544b..b906905 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -4,6 +4,7 @@ import collections.abc import concurrent.futures import functools import io +import multiprocessing import os import platform import re @@ -2762,7 +2763,13 @@ class GetEventLoopTestsMixin: support.skip_if_broken_multiprocessing_synchronize() async def main(): - pool = concurrent.futures.ProcessPoolExecutor() + if multiprocessing.get_start_method() == 'fork': + # Avoid 'fork' DeprecationWarning. + mp_context = multiprocessing.get_context('forkserver') + else: + mp_context = None + pool = concurrent.futures.ProcessPoolExecutor( + mp_context=mp_context) result = await self.loop.run_in_executor( pool, _test_get_event_loop_new_process__sub_proc) pool.shutdown() diff --git a/Lib/test/test_concurrent_futures.py b/Lib/test/test_concurrent_futures.py index b3520ae..4493cd3 100644 --- a/Lib/test/test_concurrent_futures.py +++ b/Lib/test/test_concurrent_futures.py @@ -18,6 +18,7 @@ import sys import threading import time import unittest +import warnings import weakref from pickle import PicklingError @@ -571,6 +572,24 @@ class ProcessPoolShutdownTest(ExecutorShutdownTest): assert all([r == abs(v) for r, v in zip(res, range(-5, 5))]) +@unittest.skipIf(mp.get_all_start_methods()[0] != "fork", "non-fork default.") +class ProcessPoolExecutorDefaultForkWarning(unittest.TestCase): + def test_fork_default_warns(self): + with self.assertWarns(mp.context.DefaultForkDeprecationWarning): + with futures.ProcessPoolExecutor(2): + pass + + def test_explicit_fork_does_not_warn(self): + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("ignore") + warnings.filterwarnings( + 'always', category=mp.context.DefaultForkDeprecationWarning) + ctx = mp.get_context("fork") # Non-default fork context. + with futures.ProcessPoolExecutor(2, mp_context=ctx): + pass + self.assertEqual(len(ws), 0, msg=[str(x) for x in ws]) + + create_executor_tests(ProcessPoolShutdownTest, executor_mixins=(ProcessPoolForkMixin, ProcessPoolForkserverMixin, diff --git a/Lib/test/test_fcntl.py b/Lib/test/test_fcntl.py index fc8c393..113c780 100644 --- a/Lib/test/test_fcntl.py +++ b/Lib/test/test_fcntl.py @@ -1,11 +1,11 @@ """Test program for the fcntl C module. """ +import multiprocessing import platform import os import struct import sys import unittest -from multiprocessing import Process from test.support import verbose, cpython_only from test.support.import_helper import import_module from test.support.os_helper import TESTFN, unlink @@ -160,7 +160,8 @@ class TestFcntl(unittest.TestCase): self.f = open(TESTFN, 'wb+') cmd = fcntl.LOCK_EX | fcntl.LOCK_NB fcntl.lockf(self.f, cmd) - p = Process(target=try_lockf_on_other_process_fail, args=(TESTFN, cmd)) + mp = multiprocessing.get_context('spawn') + p = mp.Process(target=try_lockf_on_other_process_fail, args=(TESTFN, cmd)) p.start() p.join() fcntl.lockf(self.f, fcntl.LOCK_UN) @@ -171,7 +172,8 @@ class TestFcntl(unittest.TestCase): self.f = open(TESTFN, 'wb+') cmd = fcntl.LOCK_SH | fcntl.LOCK_NB fcntl.lockf(self.f, cmd) - p = Process(target=try_lockf_on_other_process, args=(TESTFN, cmd)) + mp = multiprocessing.get_context('spawn') + p = mp.Process(target=try_lockf_on_other_process, args=(TESTFN, cmd)) p.start() p.join() fcntl.lockf(self.f, fcntl.LOCK_UN) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 072056d..8a12d57 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -4759,8 +4759,9 @@ class LogRecordTest(BaseTest): # In other processes, processName is correct when multiprocessing in imported, # but it is (incorrectly) defaulted to 'MainProcess' otherwise (bpo-38762). import multiprocessing - parent_conn, child_conn = multiprocessing.Pipe() - p = multiprocessing.Process( + mp = multiprocessing.get_context('spawn') + parent_conn, child_conn = mp.Pipe() + p = mp.Process( target=self._extract_logrecord_process_name, args=(2, LOG_MULTI_PROCESSING, child_conn,) ) diff --git a/Lib/test/test_multiprocessing_defaults.py b/Lib/test/test_multiprocessing_defaults.py new file mode 100644 index 0000000..1da4c06 --- /dev/null +++ b/Lib/test/test_multiprocessing_defaults.py @@ -0,0 +1,82 @@ +"""Test default behavior of multiprocessing.""" + +from inspect import currentframe, getframeinfo +import multiprocessing +from multiprocessing.context import DefaultForkDeprecationWarning +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('ignore') + warnings.filterwarnings('always', category=DefaultForkDeprecationWarning) + process = multiprocessing.Process(target=do_nothing) + process.start() # warning should point here. + join_process(process) + self.assertIsInstance(ws[0].message, DefaultForkDeprecationWarning) + self.assertIn(__file__, ws[0].filename) + self.assertEqual(getframeinfo(currentframe()).lineno-4, ws[0].lineno) + self.assertIn("'fork'", str(ws[0].message)) + self.assertIn("get_context", str(ws[0].message)) + self.assertEqual(len(ws), 1, msg=[str(x) for x in ws]) + + def test_default_fork_start_method_warning_pool(self): + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('ignore') + warnings.filterwarnings('always', category=DefaultForkDeprecationWarning) + pool = multiprocessing.Pool(1) # warning should point here. + pool.terminate() + pool.join() + self.assertIsInstance(ws[0].message, DefaultForkDeprecationWarning) + self.assertIn(__file__, ws[0].filename) + self.assertEqual(getframeinfo(currentframe()).lineno-5, ws[0].lineno) + self.assertIn("'fork'", str(ws[0].message)) + self.assertIn("get_context", str(ws[0].message)) + self.assertEqual(len(ws), 1, msg=[str(x) for x in ws]) + + def test_default_fork_start_method_warning_manager(self): + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('ignore') + warnings.filterwarnings('always', category=DefaultForkDeprecationWarning) + manager = multiprocessing.Manager() # warning should point here. + manager.shutdown() + self.assertIsInstance(ws[0].message, DefaultForkDeprecationWarning) + self.assertIn(__file__, ws[0].filename) + self.assertEqual(getframeinfo(currentframe()).lineno-4, ws[0].lineno) + self.assertIn("'fork'", str(ws[0].message)) + self.assertIn("get_context", str(ws[0].message)) + self.assertEqual(len(ws), 1, msg=[str(x) for x in ws]) + + def test_no_mp_warning_when_using_explicit_fork_context(self): + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('ignore') + warnings.filterwarnings('always', category=DefaultForkDeprecationWarning) + 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() diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index 44fdca7..80e7a4d 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -533,6 +533,8 @@ class CompatPickleTests(unittest.TestCase): def test_multiprocessing_exceptions(self): module = import_helper.import_module('multiprocessing.context') for name, exc in get_exceptions(module): + if issubclass(exc, Warning): + continue with self.subTest(name): self.assertEqual(reverse_mapping('multiprocessing.context', name), ('multiprocessing', name)) diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py index 11628a2..eacb1a7 100644 --- a/Lib/test/test_re.py +++ b/Lib/test/test_re.py @@ -2431,7 +2431,8 @@ class ReTests(unittest.TestCase): input_js = '''a(function() { /////////////////////////////////////////////////////////////////// });''' - p = multiprocessing.Process(target=pattern.sub, args=('', input_js)) + mp = multiprocessing.get_context('spawn') + p = mp.Process(target=pattern.sub, args=('', input_js)) p.start() p.join(SHORT_TIMEOUT) try: |