Skip to content

Commit 77605fa

Browse files
pulkinblurb-it[bot]picnixz
authored
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>
1 parent 862fd89 commit 77605fa

File tree

6 files changed

+54
-2
lines changed

6 files changed

+54
-2
lines changed

Doc/library/multiprocessing.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,25 @@ The :mod:`multiprocessing` package mostly replicates the API of the
670670

671671
.. versionadded:: 3.3
672672

673+
.. method:: interrupt()
674+
675+
Terminate the process. Works on POSIX using the :py:const:`~signal.SIGINT` signal.
676+
Behavior on Windows is undefined.
677+
678+
By default, this terminates the child process by raising :exc:`KeyboardInterrupt`.
679+
This behavior can be altered by setting the respective signal handler in the child
680+
process :func:`signal.signal` for :py:const:`~signal.SIGINT`.
681+
682+
Note: if the child process catches and discards :exc:`KeyboardInterrupt`, the
683+
process will not be terminated.
684+
685+
Note: the default behavior will also set :attr:`exitcode` to ``1`` as if an
686+
uncaught exception was raised in the child process. To have a different
687+
:attr:`exitcode` you may simply catch :exc:`KeyboardInterrupt` and call
688+
``exit(your_code)``.
689+
690+
.. versionadded:: next
691+
673692
.. method:: terminate()
674693

675694
Terminate the process. On POSIX this is done using the :py:const:`~signal.SIGTERM` signal;

Doc/whatsnew/3.14.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,10 @@ multiprocessing
972972
The :func:`set` in :func:`multiprocessing.Manager` method is now available.
973973
(Contributed by Mingyu Park in :gh:`129949`.)
974974

975+
* Add :func:`multiprocessing.Process.interrupt` which terminates the child
976+
process by sending :py:const:`~signal.SIGINT`. This enables "finally" clauses
977+
and printing stack trace for the terminated process.
978+
(Contributed by Artem Pulkin in :gh:`131913`.)
975979

976980
operator
977981
--------

Lib/multiprocessing/popen_fork.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def _send_signal(self, sig):
5454
if self.wait(timeout=0.1) is None:
5555
raise
5656

57+
def interrupt(self):
58+
self._send_signal(signal.SIGINT)
59+
5760
def terminate(self):
5861
self._send_signal(signal.SIGTERM)
5962

Lib/multiprocessing/process.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ def start(self):
125125
del self._target, self._args, self._kwargs
126126
_children.add(self)
127127

128+
def interrupt(self):
129+
'''
130+
Terminate process; sends SIGINT signal
131+
'''
132+
self._check_closed()
133+
self._popen.interrupt()
134+
128135
def terminate(self):
129136
'''
130137
Terminate process; sends SIGTERM signal or uses TerminateProcess()

Lib/test/_test_multiprocessing.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,15 +512,20 @@ def _test_process_mainthread_native_id(cls, q):
512512
def _sleep_some(cls):
513513
time.sleep(100)
514514

515+
@classmethod
516+
def _sleep_no_int_handler(cls):
517+
signal.signal(signal.SIGINT, signal.SIG_DFL)
518+
cls._sleep_some()
519+
515520
@classmethod
516521
def _test_sleep(cls, delay):
517522
time.sleep(delay)
518523

519-
def _kill_process(self, meth):
524+
def _kill_process(self, meth, target=None):
520525
if self.TYPE == 'threads':
521526
self.skipTest('test not appropriate for {}'.format(self.TYPE))
522527

523-
p = self.Process(target=self._sleep_some)
528+
p = self.Process(target=target or self._sleep_some)
524529
p.daemon = True
525530
p.start()
526531

@@ -567,6 +572,19 @@ def handler(*args):
567572

568573
return p.exitcode
569574

575+
@unittest.skipIf(os.name == 'nt', "POSIX only")
576+
def test_interrupt(self):
577+
exitcode = self._kill_process(multiprocessing.Process.interrupt)
578+
self.assertEqual(exitcode, 1)
579+
# exit code 1 is hard-coded for uncaught exceptions
580+
# (KeyboardInterrupt in this case)
581+
# in multiprocessing.BaseProcess._bootstrap
582+
583+
@unittest.skipIf(os.name == 'nt', "POSIX only")
584+
def test_interrupt_no_handler(self):
585+
exitcode = self._kill_process(multiprocessing.Process.interrupt, target=self._sleep_no_int_handler)
586+
self.assertEqual(exitcode, -signal.SIGINT)
587+
570588
def test_terminate(self):
571589
exitcode = self._kill_process(multiprocessing.Process.terminate)
572590
self.assertEqual(exitcode, -signal.SIGTERM)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)