bpo-37076: _thread.start_new_thread() calls _PyErr_WriteUnraisableMsg… · python/cpython@8b09500

4 files changed

lines changed

Original file line numberDiff line numberDiff line change

@@ -43,12 +43,22 @@ This module defines the following constants and functions:

4343
4444

.. function:: start_new_thread(function, args[, kwargs])

4545
46-

Start a new thread and return its identifier. The thread executes the function

47-

*function* with the argument list *args* (which must be a tuple). The optional

48-

*kwargs* argument specifies a dictionary of keyword arguments. When the function

49-

returns, the thread silently exits. When the function terminates with an

50-

unhandled exception, a stack trace is printed and then the thread exits (but

51-

other threads continue to run).

46+

Start a new thread and return its identifier. The thread executes the

47+

function *function* with the argument list *args* (which must be a tuple).

48+

The optional *kwargs* argument specifies a dictionary of keyword arguments.

49+
50+

When the function returns, the thread silently exits.

51+
52+

When the function terminates with an unhandled exception,

53+

:func:`sys.unraisablehook` is called to handle the exception. The *object*

54+

attribute of the hook argument is *function*. By default, a stack trace is

55+

printed and then the thread exits (but other threads continue to run).

56+
57+

When the function raises a :exc:`SystemExit` exception, it is silently

58+

ignored.

59+
60+

.. versionchanged:: 3.8

61+

:func:`sys.unraisablehook` is now used to handle unhandled exceptions.

5262
5363
5464

.. function:: interrupt_main()

Original file line numberDiff line numberDiff line change

@@ -154,6 +154,24 @@ def mywrite(self, *args):

154154

started.acquire()

155155

self.assertIn("Traceback", stderr.getvalue())

156156
157+

def test_unraisable_exception(self):

158+

def task():

159+

started.release()

160+

raise ValueError("task failed")

161+
162+

started = thread.allocate_lock()

163+

with support.catch_unraisable_exception() as cm:

164+

with support.wait_threads_exit():

165+

started.acquire()

166+

thread.start_new_thread(task, ())

167+

started.acquire()

168+
169+

self.assertEqual(str(cm.unraisable.exc_value), "task failed")

170+

self.assertIs(cm.unraisable.object, task)

171+

self.assertEqual(cm.unraisable.err_msg,

172+

"Exception ignored in thread started by")

173+

self.assertIsNotNone(cm.unraisable.exc_traceback)

174+
157175
158176

class Barrier:

159177

def __init__(self, num_threads):

Original file line numberDiff line numberDiff line change

@@ -0,0 +1,3 @@

1+

:func:`_thread.start_new_thread` now logs uncaught exception raised by the

2+

function using :func:`sys.unraisablehook`, rather than :func:`sys.excepthook`,

3+

so the hook gets access to the function which raised the exception.

Original file line numberDiff line numberDiff line change

@@ -1002,25 +1002,15 @@ t_bootstrap(void *boot_raw)

10021002

res = PyObject_Call(boot->func, boot->args, boot->keyw);

10031003

if (res == NULL) {

10041004

if (PyErr_ExceptionMatches(PyExc_SystemExit))

1005+

/* SystemExit is ignored silently */

10051006

PyErr_Clear();

10061007

else {

1007-

PyObject *file;

1008-

PyObject *exc, *value, *tb;

1009-

PySys_WriteStderr(

1010-

"Unhandled exception in thread started by ");

1011-

PyErr_Fetch(&exc, &value, &tb);

1012-

file = _PySys_GetObjectId(&PyId_stderr);

1013-

if (file != NULL && file != Py_None)

1014-

PyFile_WriteObject(boot->func, file, 0);

1015-

else

1016-

PyObject_Print(boot->func, stderr, 0);

1017-

PySys_WriteStderr("\n");

1018-

PyErr_Restore(exc, value, tb);

1019-

PyErr_PrintEx(0);

1008+

_PyErr_WriteUnraisableMsg("in thread started by", boot->func);

10201009

}

10211010

}

1022-

else

1011+

else {

10231012

Py_DECREF(res);

1013+

}

10241014

Py_DECREF(boot->func);

10251015

Py_DECREF(boot->args);

10261016

Py_XDECREF(boot->keyw);