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
8 changes: 8 additions & 0 deletions Doc/library/threading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ since it is impossible to detect the termination of alien threads.
base class constructor (``Thread.__init__()``) before doing anything else to
the thread.

Daemon threads must not be used in subinterpreters.

.. versionchanged:: 3.3
Added the *daemon* argument.

Expand All @@ -286,6 +288,12 @@ since it is impossible to detect the termination of alien threads.
This method will raise a :exc:`RuntimeError` if called more than once
on the same thread object.

Raise a :exc:`RuntimeError` if the thread is a daemon thread and the
method is called from a subinterpreter.

.. versionchanged:: 3.9
In a subinterpreter, spawning a daemon thread now raises an exception.

.. method:: run()

Method representing the thread's activity.
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ New Modules
Improved Modules
================

threading
---------

In a subinterpreter, spawning a daemon thread now raises an exception. Daemon
threads were never supported in subinterpreters. Previously, the subinterpreter
finalization crashed with a Pyton fatal error if a daemon thread was still
running.


Optimizations
=============
Expand Down
4 changes: 4 additions & 0 deletions Lib/_dummy_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,7 @@ def interrupt_main():
else:
global _interrupt
_interrupt = True


def _is_main_interpreter():
return True
66 changes: 37 additions & 29 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os
import subprocess
import signal
import textwrap

from test import lock_tests
from test import support
Expand Down Expand Up @@ -928,14 +929,19 @@ def test_clear_threads_states_after_fork(self):


class SubinterpThreadingTests(BaseTestCase):
def pipe(self):
r, w = os.pipe()
self.addCleanup(os.close, r)
self.addCleanup(os.close, w)
if hasattr(os, 'set_blocking'):
os.set_blocking(r, False)
return (r, w)

def test_threads_join(self):
# Non-daemon threads should be joined at subinterpreter shutdown
# (issue #18808)
r, w = os.pipe()
self.addCleanup(os.close, r)
self.addCleanup(os.close, w)
code = r"""if 1:
r, w = self.pipe()
code = textwrap.dedent(r"""
import os
import random
import threading
Expand All @@ -953,7 +959,7 @@ def f():

threading.Thread(target=f).start()
random_sleep()
""" % (w,)
""" % (w,))
ret = test.support.run_in_subinterp(code)
self.assertEqual(ret, 0)
# The thread was joined properly.
Expand All @@ -964,10 +970,8 @@ def test_threads_join_2(self):
# Python code returned but before the thread state is deleted.
# To achieve this, we register a thread-local object which sleeps
# a bit when deallocated.
r, w = os.pipe()
self.addCleanup(os.close, r)
self.addCleanup(os.close, w)
code = r"""if 1:
r, w = self.pipe()
code = textwrap.dedent(r"""
import os
import random
import threading
Expand All @@ -992,34 +996,38 @@ def f():

threading.Thread(target=f).start()
random_sleep()
""" % (w,)
""" % (w,))
ret = test.support.run_in_subinterp(code)
self.assertEqual(ret, 0)
# The thread was joined properly.
self.assertEqual(os.read(r, 1), b"x")

@cpython_only
def test_daemon_threads_fatal_error(self):
subinterp_code = r"""if 1:
import os
def test_daemon_thread(self):
r, w = self.pipe()
code = textwrap.dedent(f"""
import threading
import time
import sys

def f():
# Make sure the daemon thread is still running when
# Py_EndInterpreter is called.
time.sleep(10)
threading.Thread(target=f, daemon=True).start()
"""
script = r"""if 1:
import _testcapi
channel = open({w}, "w", closefd=False)

def func():
pass

thread = threading.Thread(target=func, daemon=True)
try:
thread.start()
except RuntimeError as exc:
print("ok: %s" % exc, file=channel, flush=True)
else:
thread.join()
print("fail: RuntimeError not raised", file=channel, flush=True)
""")
ret = test.support.run_in_subinterp(code)
self.assertEqual(ret, 0)

_testcapi.run_in_subinterp(%r)
""" % (subinterp_code,)
with test.support.SuppressCrashReport():
rc, out, err = assert_python_failure("-c", script)
self.assertIn("Fatal Python error: Py_EndInterpreter: "
"not the last thread", err.decode())
msg = os.read(r, 100).decode().rstrip()
self.assertEqual("ok: daemon thread are not supported "
"in subinterpreters", msg)


class ThreadingExceptionTests(BaseTestCase):
Expand Down
6 changes: 6 additions & 0 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
_allocate_lock = _thread.allocate_lock
_set_sentinel = _thread._set_sentinel
get_ident = _thread.get_ident
_is_main_interpreter = _thread._is_main_interpreter
try:
get_native_id = _thread.get_native_id
_HAVE_THREAD_NATIVE_ID = True
Expand Down Expand Up @@ -846,6 +847,11 @@ def start(self):

if self._started.is_set():
raise RuntimeError("threads can only be started once")

if self.daemon and not _is_main_interpreter():
raise RuntimeError("daemon thread are not supported "
"in subinterpreters")

with _active_limbo_lock:
_limbo[self] = self
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
In a subinterpreter, spawning a daemon thread now raises an exception. Daemon
threads were never supported in subinterpreters. Previously, the subinterpreter
finalization crashed with a Pyton fatal error if a daemon thread was still
running.
24 changes: 24 additions & 0 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
#include "structmember.h" /* offsetof */
#include "pythread.h"

#include "clinic/_threadmodule.c.h"

/*[clinic input]
module _thread
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=be8dbe5cc4b16df7]*/


static PyObject *ThreadError;
static PyObject *str_dict;

Expand Down Expand Up @@ -1442,6 +1450,21 @@ PyDoc_STRVAR(excepthook_doc,
\n\
Handle uncaught Thread.run() exception.");

/*[clinic input]
_thread._is_main_interpreter

Return True if the current interpreter is the main Python interpreter.
[clinic start generated code]*/

static PyObject *
_thread__is_main_interpreter_impl(PyObject *module)
/*[clinic end generated code: output=7dd82e1728339adc input=cc1eb00fd4598915]*/
{
_PyRuntimeState *runtime = &_PyRuntime;
PyInterpreterState *interp = _PyRuntimeState_GetThreadState(runtime)->interp;
return PyBool_FromLong(interp == runtime->interpreters.main);
}

static PyMethodDef thread_methods[] = {
{"start_new_thread", (PyCFunction)thread_PyThread_start_new_thread,
METH_VARARGS, start_new_doc},
Expand Down Expand Up @@ -1471,6 +1494,7 @@ static PyMethodDef thread_methods[] = {
METH_NOARGS, _set_sentinel_doc},
{"_excepthook", thread_excepthook,
METH_O, excepthook_doc},
_THREAD__IS_MAIN_INTERPRETER_METHODDEF
{NULL, NULL} /* sentinel */
};

Expand Down
22 changes: 22 additions & 0 deletions Modules/clinic/_threadmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.