]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-84559: Deprecate fork being the multiprocessing default. (#100618)
authorGregory P. Smith <greg@krypto.org>
Thu, 2 Feb 2023 23:50:35 +0000 (15:50 -0800)
committerGitHub <noreply@github.com>
Thu, 2 Feb 2023 23:50:35 +0000 (15:50 -0800)
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.

16 files changed:
Doc/library/concurrent.futures.rst
Doc/library/multiprocessing.rst
Doc/whatsnew/3.12.rst
Lib/compileall.py
Lib/concurrent/futures/process.py
Lib/multiprocessing/context.py
Lib/test/_test_multiprocessing.py
Lib/test/_test_venv_multiprocessing.py
Lib/test/test_asyncio/test_events.py
Lib/test/test_concurrent_futures.py
Lib/test/test_fcntl.py
Lib/test/test_logging.py
Lib/test/test_multiprocessing_defaults.py [new file with mode: 0644]
Lib/test/test_pickle.py
Lib/test/test_re.py
Misc/NEWS.d/next/Library/2023-01-01-01-19-33.gh-issue-84559.zEjsEJ.rst [new file with mode: 0644]

index 8106cc235e5a3c027ae965feab84c5f1d0fe0a90..10cffdaee0bb8c84bea74c544197b9e536569726 100644 (file)
@@ -250,9 +250,10 @@ to a :class:`ProcessPoolExecutor` will result in deadlock.
    then :exc:`ValueError` will be raised. If *max_workers* is ``None``, then
    the default chosen will be at most ``61``, even if more processors are
    available.
-   *mp_context* can be a multiprocessing context or None. It will be used to
-   launch the workers. If *mp_context* is ``None`` or not given, the default
-   multiprocessing context is used.
+   *mp_context* can be a :mod:`multiprocessing` context or ``None``. It will be
+   used to launch the workers. If *mp_context* is ``None`` or not given, the
+   default :mod:`multiprocessing` context is used.
+   See :ref:`multiprocessing-start-methods`.
 
    *initializer* is an optional callable that is called at the start of
    each worker process; *initargs* is a tuple of arguments passed to the
@@ -284,6 +285,13 @@ to a :class:`ProcessPoolExecutor` will result in deadlock.
       The *max_tasks_per_child* argument was added to allow users to
       control the lifetime of workers in the pool.
 
+   .. versionchanged:: 3.12
+      The implicit use of the :mod:`multiprocessing` *fork* start method as a
+      platform default (see :ref:`multiprocessing-start-methods`) now raises a
+      :exc:`DeprecationWarning`. The default will change in Python 3.14.
+      Code that requires *fork* should explicitly specify that when creating
+      their :class:`ProcessPoolExecutor` by passing a
+      ``mp_context=multiprocessing.get_context('fork')`` parameter.
 
 .. _processpoolexecutor-example:
 
index b5ceeb796f8f2f25e410c3fa9563450c8932114a..c60b229ae2d07ef6cd358bc44fc4b7e5401262cf 100644 (file)
@@ -19,7 +19,7 @@ offers both local and remote concurrency, effectively side-stepping the
 :term:`Global Interpreter Lock <global interpreter lock>` by using
 subprocesses instead of threads.  Due
 to this, the :mod:`multiprocessing` module allows the programmer to fully
-leverage multiple processors on a given machine.  It runs on both Unix and
+leverage multiple processors on a given machine.  It runs on both POSIX and
 Windows.
 
 The :mod:`multiprocessing` module also introduces APIs which do not have
@@ -99,11 +99,11 @@ necessary, see :ref:`multiprocessing-programming`.
 
 
 
+.. _multiprocessing-start-methods:
+
 Contexts and start methods
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-.. _multiprocessing-start-methods:
-
 Depending on the platform, :mod:`multiprocessing` supports three ways
 to start a process.  These *start methods* are
 
@@ -115,7 +115,7 @@ to start a process.  These *start methods* are
     will not be inherited.  Starting a process using this method is
     rather slow compared to using *fork* or *forkserver*.
 
-    Available on Unix and Windows.  The default on Windows and macOS.
+    Available on POSIX and Windows platforms.  The default on Windows and macOS.
 
   *fork*
     The parent process uses :func:`os.fork` to fork the Python
@@ -124,32 +124,39 @@ to start a process.  These *start methods* are
     inherited by the child process.  Note that safely forking a
     multithreaded process is problematic.
 
-    Available on Unix only.  The default on Unix.
+    Available on POSIX systems.  Currently the default on POSIX except macOS.
 
   *forkserver*
     When the program starts and selects the *forkserver* start method,
-    a server process is started.  From then on, whenever a new process
+    a server process is spawned.  From then on, whenever a new process
     is needed, the parent process connects to the server and requests
-    that it fork a new process.  The fork server process is single
-    threaded so it is safe for it to use :func:`os.fork`.  No
-    unnecessary resources are inherited.
+    that it fork a new process.  The fork server process is single threaded
+    unless system libraries or preloaded imports spawn threads as a
+    side-effect so it is generally safe for it to use :func:`os.fork`.
+    No unnecessary resources are inherited.
 
-    Available on Unix platforms which support passing file descriptors
-    over Unix pipes.
+    Available on POSIX platforms which support passing file descriptors
+    over Unix pipes such as Linux.
+
+.. versionchanged:: 3.12
+   Implicit use of the *fork* start method as the default now raises a
+   :exc:`DeprecationWarning`. Code that requires it should explicitly
+   specify *fork* via :func:`get_context` or :func:`set_start_method`.
+   The default will change away from *fork* in 3.14.
 
 .. versionchanged:: 3.8
 
    On macOS, the *spawn* start method is now the default.  The *fork* start
    method should be considered unsafe as it can lead to crashes of the
-   subprocess. See :issue:`33725`.
+   subprocess as macOS system libraries may start threads. See :issue:`33725`.
 
 .. versionchanged:: 3.4
-   *spawn* added on all Unix platforms, and *forkserver* added for
-   some Unix platforms.
+   *spawn* added on all POSIX platforms, and *forkserver* added for
+   some POSIX platforms.
    Child processes no longer inherit all of the parents inheritable
    handles on Windows.
 
-On Unix using the *spawn* or *forkserver* start methods will also
+On POSIX using the *spawn* or *forkserver* start methods will also
 start a *resource tracker* process which tracks the unlinked named
 system resources (such as named semaphores or
 :class:`~multiprocessing.shared_memory.SharedMemory` objects) created
@@ -211,10 +218,10 @@ library user.
 
 .. warning::
 
-   The ``'spawn'`` and ``'forkserver'`` start methods cannot currently
+   The ``'spawn'`` and ``'forkserver'`` start methods generally cannot
    be used with "frozen" executables (i.e., binaries produced by
-   packages like **PyInstaller** and **cx_Freeze**) on Unix.
-   The ``'fork'`` start method does work.
+   packages like **PyInstaller** and **cx_Freeze**) on POSIX systems.
+   The ``'fork'`` start method may work if code does not use threads.
 
 
 Exchanging objects between processes
@@ -629,14 +636,14 @@ The :mod:`multiprocessing` package mostly replicates the API of the
       calling :meth:`join()` is simpler.
 
       On Windows, this is an OS handle usable with the ``WaitForSingleObject``
-      and ``WaitForMultipleObjects`` family of API calls.  On Unix, this is
+      and ``WaitForMultipleObjects`` family of API calls.  On POSIX, this is
       a file descriptor usable with primitives from the :mod:`select` module.
 
       .. versionadded:: 3.3
 
    .. method:: terminate()
 
-      Terminate the process.  On Unix this is done using the ``SIGTERM`` signal;
+      Terminate the process.  On POSIX this is done using the ``SIGTERM`` signal;
       on Windows :c:func:`TerminateProcess` is used.  Note that exit handlers and
       finally clauses, etc., will not be executed.
 
@@ -653,7 +660,7 @@ The :mod:`multiprocessing` package mostly replicates the API of the
 
    .. method:: kill()
 
-      Same as :meth:`terminate()` but using the ``SIGKILL`` signal on Unix.
+      Same as :meth:`terminate()` but using the ``SIGKILL`` signal on POSIX.
 
       .. versionadded:: 3.7
 
@@ -676,16 +683,17 @@ The :mod:`multiprocessing` package mostly replicates the API of the
    .. doctest::
 
        >>> import multiprocessing, time, signal
-       >>> p = multiprocessing.Process(target=time.sleep, args=(1000,))
+       >>> mp_context = multiprocessing.get_context('spawn')
+       >>> p = mp_context.Process(target=time.sleep, args=(1000,))
        >>> print(p, p.is_alive())
-       <Process ... initial> False
+       <...Process ... initial> False
        >>> p.start()
        >>> print(p, p.is_alive())
-       <Process ... started> True
+       <...Process ... started> True
        >>> p.terminate()
        >>> time.sleep(0.1)
        >>> print(p, p.is_alive())
-       <Process ... stopped exitcode=-SIGTERM> False
+       <...Process ... stopped exitcode=-SIGTERM> False
        >>> p.exitcode == -signal.SIGTERM
        True
 
@@ -815,7 +823,7 @@ For an example of the usage of queues for interprocess communication see
       Return the approximate size of the queue.  Because of
       multithreading/multiprocessing semantics, this number is not reliable.
 
-      Note that this may raise :exc:`NotImplementedError` on Unix platforms like
+      Note that this may raise :exc:`NotImplementedError` on platforms like
       macOS where ``sem_getvalue()`` is not implemented.
 
    .. method:: empty()
@@ -1034,9 +1042,8 @@ Miscellaneous
 
    Returns a list of the supported start methods, the first of which
    is the default.  The possible start methods are ``'fork'``,
-   ``'spawn'`` and ``'forkserver'``.  On Windows only ``'spawn'`` is
-   available.  On Unix ``'fork'`` and ``'spawn'`` are always
-   supported, with ``'fork'`` being the default.
+   ``'spawn'`` and ``'forkserver'``.  Not all platforms support all
+   methods.  See :ref:`multiprocessing-start-methods`.
 
    .. versionadded:: 3.4
 
@@ -1048,7 +1055,7 @@ Miscellaneous
    If *method* is ``None`` then the default context is returned.
    Otherwise *method* should be ``'fork'``, ``'spawn'``,
    ``'forkserver'``.  :exc:`ValueError` is raised if the specified
-   start method is not available.
+   start method is not available.  See :ref:`multiprocessing-start-methods`.
 
    .. versionadded:: 3.4
 
@@ -1062,8 +1069,7 @@ Miscellaneous
    is true then ``None`` is returned.
 
    The return value can be ``'fork'``, ``'spawn'``, ``'forkserver'``
-   or ``None``.  ``'fork'`` is the default on Unix, while ``'spawn'`` is
-   the default on Windows and macOS.
+   or ``None``.  See :ref:`multiprocessing-start-methods`.
 
 .. versionchanged:: 3.8
 
@@ -1084,11 +1090,26 @@ Miscellaneous
    before they can create child processes.
 
    .. versionchanged:: 3.4
-      Now supported on Unix when the ``'spawn'`` start method is used.
+      Now supported on POSIX when the ``'spawn'`` start method is used.
 
    .. versionchanged:: 3.11
       Accepts a :term:`path-like object`.
 
+.. function:: set_forkserver_preload(module_names)
+
+   Set a list of module names for the forkserver main process to attempt to
+   import so that their already imported state is inherited by forked
+   processes. Any :exc:`ImportError` when doing so is silently ignored.
+   This can be used as a performance enhancement to avoid repeated work
+   in every process.
+
+   For this to work, it must be called before the forkserver process has been
+   launched (before creating a :class:`Pool` or starting a :class:`Process`).
+
+   Only meaningful when using the ``'forkserver'`` start method.
+
+   .. versionadded:: 3.4
+
 .. function:: set_start_method(method, force=False)
 
    Set the method which should be used to start child processes.
@@ -1102,6 +1123,8 @@ Miscellaneous
    protected inside the ``if __name__ == '__main__'`` clause of the
    main module.
 
+   See :ref:`multiprocessing-start-methods`.
+
    .. versionadded:: 3.4
 
 .. note::
@@ -1906,7 +1929,8 @@ their parent process exits.  The manager classes are defined in the
 
    .. doctest::
 
-    >>> manager = multiprocessing.Manager()
+    >>> mp_context = multiprocessing.get_context('spawn')
+    >>> manager = mp_context.Manager()
     >>> Global = manager.Namespace()
     >>> Global.x = 10
     >>> Global.y = 'hello'
@@ -2018,8 +2042,8 @@ the proxy).  In this way, a proxy can be used just like its referent can:
 
 .. doctest::
 
-   >>> from multiprocessing import Manager
-   >>> manager = Manager()
+   >>> mp_context = multiprocessing.get_context('spawn')
+   >>> manager = mp_context.Manager()
    >>> l = manager.list([i*i for i in range(10)])
    >>> print(l)
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
@@ -2520,7 +2544,7 @@ multiple connections at the same time.
    *timeout* is ``None`` then it will block for an unlimited period.
    A negative timeout is equivalent to a zero timeout.
 
-   For both Unix and Windows, an object can appear in *object_list* if
+   For both POSIX and Windows, an object can appear in *object_list* if
    it is
 
    * a readable :class:`~multiprocessing.connection.Connection` object;
@@ -2531,7 +2555,7 @@ multiple connections at the same time.
    A connection or socket object is ready when there is data available
    to be read from it, or the other end has been closed.
 
-   **Unix**: ``wait(object_list, timeout)`` almost equivalent
+   **POSIX**: ``wait(object_list, timeout)`` almost equivalent
    ``select.select(object_list, [], [], timeout)``.  The difference is
    that, if :func:`select.select` is interrupted by a signal, it can
    raise :exc:`OSError` with an error number of ``EINTR``, whereas
@@ -2803,7 +2827,7 @@ Thread safety of proxies
 
 Joining zombie processes
 
-    On Unix when a process finishes but has not been joined it becomes a zombie.
+    On POSIX when a process finishes but has not been joined it becomes a zombie.
     There should never be very many because each time a new process starts (or
     :func:`~multiprocessing.active_children` is called) all completed processes
     which have not yet been joined will be joined.  Also calling a finished
@@ -2866,7 +2890,7 @@ Joining processes that use queues
 
 Explicitly pass resources to child processes
 
-    On Unix using the *fork* start method, a child process can make
+    On POSIX using the *fork* start method, a child process can make
     use of a shared resource created in a parent process using a
     global resource.  However, it is better to pass the object as an
     argument to the constructor for the child process.
index a071159b800a34d81c44f36ddf5effaffb30a196..e675fada339a1ee9c8d3bcb54a702c03143c6740 100644 (file)
@@ -440,6 +440,11 @@ Deprecated
   warning at compile time. This field will be removed in Python 3.14.
   (Contributed by Ramvikrams and Kumar Aditya in :gh:`101193`. PEP by Ken Jin.)
 
+* Use of the implicit default ``'fork'`` start method for
+  :mod:`multiprocessing` and :class:`concurrent.futures.ProcessPoolExecutor`
+  now emits a :exc:`DeprecationWarning` on Linux and other non-macOS POSIX
+  systems. Avoid this by explicitly specifying a start method.
+  See :ref:`multiprocessing-start-methods`.
 
 Pending Removal in Python 3.13
 ------------------------------
@@ -505,6 +510,9 @@ Pending Removal in Python 3.14
 * Testing the truth value of an :class:`xml.etree.ElementTree.Element`
   is deprecated and will raise an exception in Python 3.14.
 
+* The default :mod:`multiprocessing` start method will change to one of either
+  ``'forkserver'`` or ``'spawn'`` on all platforms for which ``'fork'`` remains
+  the default per :gh:`84559`.
 
 Pending Removal in Future Versions
 ----------------------------------
index a388931fb5a99da63f517a09b61683fd33d7d0de..d394156cedc4e717da0392f8f109b0f5214d27c4 100644 (file)
@@ -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,
index 7e2f5fa30e8264132d97c5a72a04d594b83cd6f8..257dd02fbc6cceb2c5310121dcb9c0574d27b264 100644 (file)
@@ -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
index b1960ea296fe208935b706898f3d564b9a598d6e..010a920540e844d555a95e4421608ec4d5846e75 100644 (file)
@@ -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:
 
index 2fa75eb4d113118fc2a188d16ac02439a90965a9..e4a60a4d674607b843f7a569ae0310cabd73bd7c 100644 (file)
@@ -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'))
index af72e915ba52bbe4ff97a470b7adee9341767dab..044a0c6cd3f5ca0a46622f29de270b958803604d 100644 (file)
@@ -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()
index 214544b89bc558463ccd9da767be0dad69a3cc9e..b9069056c3a4367d862b35238c3e5cb56e5c0d72 100644 (file)
@@ -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()
index b3520ae3994e034a7865ce48cc683a6cc34efd45..4493cd312528d6a8fb046b5423facff91d56d041 100644 (file)
@@ -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,
index fc8c39365f12b795bbc20be50a398e8a2cb02247..113c7802821dd45a16a2e6dfa8ec287f689d2e33 100644 (file)
@@ -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)
index 072056d3722106a095311b38e15aa480f2092dbd..8a12d570f26f1361cd213785437c63c9bdf34244 100644 (file)
@@ -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 (file)
index 0000000..1da4c06
--- /dev/null
@@ -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()
index 44fdca7a6b1688b796483e8e4873eb0bf9b246e5..80e7a4d23a4ba8532a251943af8b6788a0ca93cc 100644 (file)
@@ -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))
index 11628a236ade9a3b72989420dc7d7945df99e62b..eacb1a7c82a54d6a632c55b83380731011ad904a 100644 (file)
@@ -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:
diff --git a/Misc/NEWS.d/next/Library/2023-01-01-01-19-33.gh-issue-84559.zEjsEJ.rst b/Misc/NEWS.d/next/Library/2023-01-01-01-19-33.gh-issue-84559.zEjsEJ.rst
new file mode 100644 (file)
index 0000000..3793e0f
--- /dev/null
@@ -0,0 +1,11 @@
+The :mod:`multiprocessing` module and
+:class:`concurrent.futures.ProcessPoolExecutor` will emit a
+:exc:`DeprecationWarning` on Linux and other non-macOS POSIX systems when
+the default multiprocessing start method of ``'fork'`` is used implicitly
+rather than being explicitly specified through a
+:func:`multiprocessing.get_context` context.
+
+This is in preparation for default start method to change in Python 3.14 to
+a default that is safe for multithreaded applications.
+
+Windows and macOS are unaffected as their default start method is ``spawn``.