Skip to content

Commit b47b9e8

Browse files
committed
Test & docs
1 parent 2f6139f commit b47b9e8

File tree

4 files changed

+68
-7
lines changed

4 files changed

+68
-7
lines changed

Doc/c-api/exceptions.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,8 @@ Signal Handling
699699
700700
- Executing a pending :ref:`remote debugger <remote-debugging>` script.
701701
702+
- Raise the exception set by :c:func:`PyThreadState_SetAsyncExc`.
703+
702704
If any handler raises an exception, immediately return ``-1`` with that
703705
exception set.
704706
Any remaining interruptions are left to be processed on the next
@@ -714,6 +716,9 @@ Signal Handling
714716
This function may now execute a remote debugger script, if remote
715717
debugging is enabled.
716718
719+
.. versionchanged:: next
720+
The exception set by :c:func:`PyThreadState_SetAsyncExc` is now raised.
721+
717722
718723
.. c:function:: void PyErr_SetInterrupt()
719724

Doc/c-api/threads.rst

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -699,13 +699,25 @@ pointer and a void pointer argument.
699699
700700
.. c:function:: int PyThreadState_SetAsyncExc(unsigned long id, PyObject *exc)
701701
702-
Asynchronously raise an exception in a thread. The *id* argument is the thread
703-
id of the target thread; *exc* is the exception object to be raised. This
704-
function does not steal any references to *exc*. To prevent naive misuse, you
705-
must write your own C extension to call this. Must be called with an :term:`attached thread state`.
706-
Returns the number of thread states modified; this is normally one, but will be
707-
zero if the thread id isn't found. If *exc* is ``NULL``, the pending
708-
exception (if any) for the thread is cleared. This raises no exceptions.
702+
Schedule an exception to be raised asynchronously in a thread.
703+
If the thread has a previously scheduled exception, it is overwritten.
704+
705+
The *id* argument is the thread id of the target thread, as returned by
706+
:c:func:`PyThread_get_thread_ident`.
707+
*exc* is the class of the exception to be raised, or ``NULL`` to clear
708+
the pending exception (if any).
709+
710+
Return the number of affected thread states.
711+
This is normally ``1`` if *id* is found, even when no change was
712+
made (the given *exc* was already pending, or *exc* is ``NULL`` but
713+
no exception is pending).
714+
If the thread id isn't found, return ``0``. This raises no exceptions.
715+
716+
To prevent naive misuse, you must write your own C extension to call this.
717+
This function must be called with an :term:`attached thread state`.
718+
This function does not steal any references to *exc*.
719+
This function does not necessarily interrupt system calls such as
720+
:py:func:`~time.sleep`.
709721
710722
.. versionchanged:: 3.7
711723
The type of the *id* parameter changed from :c:expr:`long` to

Lib/test/test_threading.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,48 @@ def run(self):
412412
t.join()
413413
# else the thread is still running, and we have no way to kill it
414414

415+
@cpython_only
416+
@unittest.skipUnless(hasattr(signal, "pthread_kill"), "need pthread_kill")
417+
@unittest.skipUnless(hasattr(signal, "SIGUSR1"), "need SIGUSR1")
418+
def test_PyThreadState_SetAsyncExc_interrupts_sleep(self):
419+
_testcapi = import_module("_testlimitedcapi")
420+
421+
worker_started = threading.Event()
422+
423+
class InjectedException(Exception):
424+
"""Custom exception for testing"""
425+
426+
caught_exception = None
427+
428+
def catch_exception():
429+
nonlocal caught_exception
430+
day_as_seconds = 60 * 60 * 24
431+
try:
432+
worker_started.set()
433+
time.sleep(day_as_seconds)
434+
except InjectedException as exc:
435+
caught_exception = exc
436+
437+
thread = threading.Thread(target=catch_exception)
438+
thread.start()
439+
worker_started.wait()
440+
441+
signal.signal(signal.SIGUSR1, lambda sig, frame: None)
442+
443+
result = _testcapi.threadstate_set_async_exc(
444+
thread.ident, InjectedException)
445+
self.assertEqual(result, 1)
446+
447+
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
448+
signal.pthread_kill(thread.ident, signal.SIGUSR1)
449+
if not thread.is_alive():
450+
break
451+
452+
thread.join()
453+
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
454+
455+
self.assertIsInstance(caught_exception, InjectedException)
456+
415457
def test_limbo_cleanup(self):
416458
# Issue 7481: Failure to start thread should cleanup the limbo map.
417459
def fail_new_thread(*args, **kwargs):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:c:func:`PyErr_CheckSignals` now raises the exception scheduled by
2+
:c:func:`PyThreadState_SetAsyncExc`, if any.

0 commit comments

Comments
 (0)