summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorGregory P. Smith <greg@krypto.org>2023-02-02 23:50:35 (GMT)
committerGitHub <noreply@github.com>2023-02-02 23:50:35 (GMT)
commit0ca67e6313c11263ecaef7ce182308eeb5aa6814 (patch)
treeff302df77417456ee4dcf94082b6d942e320b13a /Lib
parent618b7a8260bb40290d6551f24885931077309590 (diff)
downloadcpython-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.py8
-rw-r--r--Lib/concurrent/futures/process.py23
-rw-r--r--Lib/multiprocessing/context.py29
-rw-r--r--Lib/test/_test_multiprocessing.py21
-rw-r--r--Lib/test/_test_venv_multiprocessing.py1
-rw-r--r--Lib/test/test_asyncio/test_events.py9
-rw-r--r--Lib/test/test_concurrent_futures.py19
-rw-r--r--Lib/test/test_fcntl.py8
-rw-r--r--Lib/test/test_logging.py5
-rw-r--r--Lib/test/test_multiprocessing_defaults.py82
-rw-r--r--Lib/test/test_pickle.py2
-rw-r--r--Lib/test/test_re.py3
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: