Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Doc/library/multiprocessing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,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
--------
Expand Down
3 changes: 3 additions & 0 deletions Lib/multiprocessing/popen_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 7 additions & 0 deletions Lib/multiprocessing/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
22 changes: 20 additions & 2 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting... but probably went unnoticed because these are wholly multiprocessing managed processes so the exit code reflecting the SIGINT signal was never important there like it is for our main python process which IIRC jumps through hoops to turn KeyboardInterrupt back into a SIGINT exit status.


@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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading