https://github.com/python/cpython/commit/77605fa3bb4e380d5448665f70d73c8d3fcc2e75 commit: 77605fa3bb4e380d5448665f70d73c8d3fcc2e75 branch: main author: pulkin <gpul...@gmail.com> committer: gpshead <g...@krypto.org> date: 2025-04-22T23:55:24-07:00 summary:
gh-131913: multiprocessing: add interrupt for POSIX (GH-132453) * multiprocessing: interrupt Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com> Co-authored-by: Bénédikt Tran <10796600+picn...@users.noreply.github.com> files: A Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst M Doc/library/multiprocessing.rst M Doc/whatsnew/3.14.rst M Lib/multiprocessing/popen_fork.py M Lib/multiprocessing/process.py M Lib/test/_test_multiprocessing.py diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 6ccc0d4aa59555..e44142a8ed3106 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -670,6 +670,25 @@ The :mod:`multiprocessing` package mostly replicates the API of the .. versionadded:: 3.3 + .. method:: interrupt() + + Terminate the process. Works on POSIX using the :py:const:`~signal.SIGINT` signal. + Behavior on Windows is undefined. + + By default, this terminates the child process by raising :exc:`KeyboardInterrupt`. + This behavior can be altered by setting the respective signal handler in the child + process :func:`signal.signal` for :py:const:`~signal.SIGINT`. + + Note: if the child process catches and discards :exc:`KeyboardInterrupt`, the + process will not be terminated. + + Note: the default behavior will also set :attr:`exitcode` to ``1`` as if an + uncaught exception was raised in the child process. To have a different + :attr:`exitcode` you may simply catch :exc:`KeyboardInterrupt` and call + ``exit(your_code)``. + + .. versionadded:: next + .. method:: terminate() Terminate the process. On POSIX this is done using the :py:const:`~signal.SIGTERM` signal; diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 904a3ce6d597e9..63a5bab7fe8fea 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -972,6 +972,10 @@ multiprocessing The :func:`set` in :func:`multiprocessing.Manager` method is now available. (Contributed by Mingyu Park in :gh:`129949`.) +* Add :func:`multiprocessing.Process.interrupt` which terminates the child + process by sending :py:const:`~signal.SIGINT`. This enables "finally" clauses + and printing stack trace for the terminated process. + (Contributed by Artem Pulkin in :gh:`131913`.) operator -------- diff --git a/Lib/multiprocessing/popen_fork.py b/Lib/multiprocessing/popen_fork.py index a57ef6bdad5ccc..7affa1b985f091 100644 --- a/Lib/multiprocessing/popen_fork.py +++ b/Lib/multiprocessing/popen_fork.py @@ -54,6 +54,9 @@ def _send_signal(self, sig): if self.wait(timeout=0.1) is None: raise + def interrupt(self): + self._send_signal(signal.SIGINT) + def terminate(self): self._send_signal(signal.SIGTERM) diff --git a/Lib/multiprocessing/process.py b/Lib/multiprocessing/process.py index b45f7df476f7d8..9db322be1aa6d6 100644 --- a/Lib/multiprocessing/process.py +++ b/Lib/multiprocessing/process.py @@ -125,6 +125,13 @@ def start(self): del self._target, self._args, self._kwargs _children.add(self) + def interrupt(self): + ''' + Terminate process; sends SIGINT signal + ''' + self._check_closed() + self._popen.interrupt() + def terminate(self): ''' Terminate process; sends SIGTERM signal or uses TerminateProcess() diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 58d8a5eae8a2ce..54b942e76d78c5 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -512,15 +512,20 @@ def _test_process_mainthread_native_id(cls, q): def _sleep_some(cls): time.sleep(100) + @classmethod + def _sleep_no_int_handler(cls): + signal.signal(signal.SIGINT, signal.SIG_DFL) + cls._sleep_some() + @classmethod def _test_sleep(cls, delay): time.sleep(delay) - def _kill_process(self, meth): + def _kill_process(self, meth, target=None): if self.TYPE == 'threads': self.skipTest('test not appropriate for {}'.format(self.TYPE)) - p = self.Process(target=self._sleep_some) + p = self.Process(target=target or self._sleep_some) p.daemon = True p.start() @@ -567,6 +572,19 @@ def handler(*args): return p.exitcode + @unittest.skipIf(os.name == 'nt', "POSIX only") + def test_interrupt(self): + exitcode = self._kill_process(multiprocessing.Process.interrupt) + self.assertEqual(exitcode, 1) + # exit code 1 is hard-coded for uncaught exceptions + # (KeyboardInterrupt in this case) + # in multiprocessing.BaseProcess._bootstrap + + @unittest.skipIf(os.name == 'nt', "POSIX only") + def test_interrupt_no_handler(self): + exitcode = self._kill_process(multiprocessing.Process.interrupt, target=self._sleep_no_int_handler) + self.assertEqual(exitcode, -signal.SIGINT) + def test_terminate(self): exitcode = self._kill_process(multiprocessing.Process.terminate) self.assertEqual(exitcode, -signal.SIGTERM) diff --git a/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst new file mode 100644 index 00000000000000..be036524bc3434 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-12-19-42-51.gh-issue-131913.twOx7K.rst @@ -0,0 +1 @@ +Add a shortcut function :func:`multiprocessing.Process.interrupt` alongside the existing :func:`multiprocessing.Process.terminate` and :func:`multiprocessing.Process.kill` for an improved control over child process termination. _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com