[Python-checkins] bpo-37266: Daemon threads are now denied in subinterpreters (GH-14049)

Victor Stinner webhook-mailer at python.org
Fri Jun 14 12:55:28 EDT 2019


https://github.com/python/cpython/commit/066e5b1a917ec2134e8997d2cadd815724314252
commit: 066e5b1a917ec2134e8997d2cadd815724314252
branch: master
author: Victor Stinner <vstinner at redhat.com>
committer: GitHub <noreply at github.com>
date: 2019-06-14T18:55:22+02:00
summary:

bpo-37266: Daemon threads are now denied in subinterpreters (GH-14049)

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.

* Add _thread._is_main_interpreter()
* threading.Thread.start() now raises RuntimeError if the thread is a
  daemon thread and the method is called from a subinterpreter.
* The _thread module now uses Argument Clinic for the new function.
* Use textwrap.dedent() in test_threading.SubinterpThreadingTests

files:
A Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst
A Modules/clinic/_threadmodule.c.h
M Doc/library/threading.rst
M Doc/whatsnew/3.9.rst
M Lib/_dummy_thread.py
M Lib/test/test_threading.py
M Lib/threading.py
M Modules/_threadmodule.c

diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst
index 9ffd5cd58179..f80eb22e18fc 100644
--- a/Doc/library/threading.rst
+++ b/Doc/library/threading.rst
@@ -280,6 +280,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.
 
@@ -294,6 +296,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.
diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst
index 999519f0ce07..ef30743b708d 100644
--- a/Doc/whatsnew/3.9.rst
+++ b/Doc/whatsnew/3.9.rst
@@ -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
 =============
diff --git a/Lib/_dummy_thread.py b/Lib/_dummy_thread.py
index a2cae54b0580..2407f9bf5ddc 100644
--- a/Lib/_dummy_thread.py
+++ b/Lib/_dummy_thread.py
@@ -161,3 +161,7 @@ def interrupt_main():
     else:
         global _interrupt
         _interrupt = True
+
+
+def _is_main_interpreter():
+    return True
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index 0a0a62bdf9bf..a04d496001e3 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -17,6 +17,7 @@
 import os
 import subprocess
 import signal
+import textwrap
 
 from test import lock_tests
 from test import support
@@ -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
@@ -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.
@@ -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
@@ -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):
diff --git a/Lib/threading.py b/Lib/threading.py
index 7c6d404bcd10..01a15a6fc075 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -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
@@ -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:
diff --git a/Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst b/Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst
new file mode 100644
index 000000000000..f41918185213
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-06-13-11-59-52.bpo-37266.goLjef.rst
@@ -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.
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
index d5e40ef999e3..9ab8e7a0ceb3 100644
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -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;
 
@@ -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},
@@ -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 */
 };
 
diff --git a/Modules/clinic/_threadmodule.c.h b/Modules/clinic/_threadmodule.c.h
new file mode 100644
index 000000000000..07ea08b1750d
--- /dev/null
+++ b/Modules/clinic/_threadmodule.c.h
@@ -0,0 +1,22 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+PyDoc_STRVAR(_thread__is_main_interpreter__doc__,
+"_is_main_interpreter($module, /)\n"
+"--\n"
+"\n"
+"Return True if the current interpreter is the main Python interpreter.");
+
+#define _THREAD__IS_MAIN_INTERPRETER_METHODDEF    \
+    {"_is_main_interpreter", (PyCFunction)_thread__is_main_interpreter, METH_NOARGS, _thread__is_main_interpreter__doc__},
+
+static PyObject *
+_thread__is_main_interpreter_impl(PyObject *module);
+
+static PyObject *
+_thread__is_main_interpreter(PyObject *module, PyObject *Py_UNUSED(ignored))
+{
+    return _thread__is_main_interpreter_impl(module);
+}
+/*[clinic end generated code: output=505840d1b9101789 input=a9049054013a1b77]*/



More information about the Python-checkins mailing list