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+picnixz@users.noreply.github.com>
This commit is contained in:
pulkin 2025-04-23 08:55:24 +02:00 committed by GitHub
parent 862fd89036
commit 77605fa3bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 54 additions and 2 deletions

View file

@ -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;

View file

@ -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
--------

View file

@ -54,6 +54,9 @@ class Popen(object):
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)

View file

@ -125,6 +125,13 @@ class BaseProcess(object):
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()

View file

@ -512,15 +512,20 @@ class _TestProcess(BaseTestCase):
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 @@ class _TestProcess(BaseTestCase):
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)

View file

@ -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.