diff --git a/Doc/library/_thread.rst b/Doc/library/_thread.rst index a6ce945c72057c..48d36e85c9e5ab 100644 --- a/Doc/library/_thread.rst +++ b/Doc/library/_thread.rst @@ -43,12 +43,22 @@ This module defines the following constants and functions: .. function:: start_new_thread(function, args[, kwargs]) - Start a new thread and return its identifier. The thread executes the function - *function* with the argument list *args* (which must be a tuple). The optional - *kwargs* argument specifies a dictionary of keyword arguments. When the function - returns, the thread silently exits. When the function terminates with an - unhandled exception, a stack trace is printed and then the thread exits (but - other threads continue to run). + Start a new thread and return its identifier. The thread executes the + function *function* with the argument list *args* (which must be a tuple). + The optional *kwargs* argument specifies a dictionary of keyword arguments. + + When the function returns, the thread silently exits. + + When the function terminates with an unhandled exception, + :func:`sys.unraisablehook` is called to handle the exception. The *object* + attribute of the hook argument is *function*. By default, a stack trace is + printed and then the thread exits (but other threads continue to run). + + When the function raises a :exc:`SystemExit` exception, it is silently + ignored. + + .. versionchanged:: 3.8 + :func:`sys.unraisablehook` is now used to handle unhandled exceptions. .. function:: interrupt_main() diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index f4eb830cf6d746..f946f7bc839928 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -154,6 +154,24 @@ def mywrite(self, *args): started.acquire() self.assertIn("Traceback", stderr.getvalue()) + def test_unraisable_exception(self): + def task(): + started.release() + raise ValueError("task failed") + + started = thread.allocate_lock() + with support.catch_unraisable_exception() as cm: + with support.wait_threads_exit(): + started.acquire() + thread.start_new_thread(task, ()) + started.acquire() + + self.assertEqual(str(cm.unraisable.exc_value), "task failed") + self.assertIs(cm.unraisable.object, task) + self.assertEqual(cm.unraisable.err_msg, + "Exception ignored in thread started by") + self.assertIsNotNone(cm.unraisable.exc_traceback) + class Barrier: def __init__(self, num_threads): diff --git a/Misc/NEWS.d/next/Library/2019-05-28-12-17-10.bpo-37076.Bk2xOs.rst b/Misc/NEWS.d/next/Library/2019-05-28-12-17-10.bpo-37076.Bk2xOs.rst new file mode 100644 index 00000000000000..2773675cb5ad71 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-28-12-17-10.bpo-37076.Bk2xOs.rst @@ -0,0 +1,3 @@ +:func:`_thread.start_new_thread` now logs uncaught exception raised by the +function using :func:`sys.unraisablehook`, rather than :func:`sys.excepthook`, +so the hook gets access to the function which raised the exception. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 680e8ca7108c18..2b1a98f81b1a4e 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -1002,25 +1002,15 @@ t_bootstrap(void *boot_raw) res = PyObject_Call(boot->func, boot->args, boot->keyw); if (res == NULL) { if (PyErr_ExceptionMatches(PyExc_SystemExit)) + /* SystemExit is ignored silently */ PyErr_Clear(); else { - PyObject *file; - PyObject *exc, *value, *tb; - PySys_WriteStderr( - "Unhandled exception in thread started by "); - PyErr_Fetch(&exc, &value, &tb); - file = _PySys_GetObjectId(&PyId_stderr); - if (file != NULL && file != Py_None) - PyFile_WriteObject(boot->func, file, 0); - else - PyObject_Print(boot->func, stderr, 0); - PySys_WriteStderr("\n"); - PyErr_Restore(exc, value, tb); - PyErr_PrintEx(0); + _PyErr_WriteUnraisableMsg("in thread started by", boot->func); } } - else + else { Py_DECREF(res); + } Py_DECREF(boot->func); Py_DECREF(boot->args); Py_XDECREF(boot->keyw);