diff --git a/Include/pystate.h b/Include/pystate.h
--- a/Include/pystate.h
+++ b/Include/pystate.h
@@ -8,6 +8,10 @@
extern "C" {
#endif
+#if defined(WITH_THREAD) && !defined(Py_LIMITED_API)
+#include "pythread.h"
+#endif
+
/* State shared between threads */
struct _ts; /* Forward */
@@ -118,6 +122,11 @@ typedef struct _ts {
int trash_delete_nesting;
PyObject *trash_delete_later;
+ /* Called when a thread state is deleted normally, but not when it
+ is destroyed after fork(). */
+ void (*on_delete)(void *);
+ void *on_delete_data;
+
/* XXX signal handlers should also be here */
} PyThreadState;
diff --git a/Lib/_dummy_thread.py b/Lib/_dummy_thread.py
--- a/Lib/_dummy_thread.py
+++ b/Lib/_dummy_thread.py
@@ -81,6 +81,10 @@ def stack_size(size=None):
raise error("setting thread stack size not supported")
return 0
+def _set_sentinel():
+ """Dummy implementation of _thread._set_sentinel()."""
+ return LockType()
+
class LockType(object):
"""Class implementing dummy implementation of _thread.LockType.
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -444,6 +444,31 @@ class ThreadTests(BaseTestCase):
self.assertEqual(out, b'')
self.assertEqual(err, b'')
+ def test_tstate_lock(self):
+ # Test an implementation detail of Thread objects.
+ started = _thread.allocate_lock()
+ finish = _thread.allocate_lock()
+ started.acquire()
+ finish.acquire()
+ def f():
+ started.release()
+ finish.acquire()
+ time.sleep(0.01)
+ # The tstate lock is None until the thread is started
+ t = threading.Thread(target=f)
+ self.assertIs(t._tstate_lock, None)
+ t.start()
+ started.acquire()
+ # The tstate lock can't be acquired when the thread is running
+ # (or suspended).
+ tstate_lock = t._tstate_lock
+ self.assertFalse(tstate_lock.acquire(timeout=0), False)
+ finish.release()
+ # When the thread ends, the state_lock can be successfully
+ # acquired.
+ self.assertTrue(tstate_lock.acquire(timeout=5), False)
+ self.assertFalse(t.is_alive())
+
class ThreadJoinOnShutdown(BaseTestCase):
diff --git a/Lib/threading.py b/Lib/threading.py
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -33,6 +33,7 @@ except ImportError:
# Rename some stuff so "from threading import *" is safe
_start_new_thread = _thread.start_new_thread
_allocate_lock = _thread.allocate_lock
+_set_sentinel = _thread._set_sentinel
get_ident = _thread.get_ident
ThreadError = _thread.error
try:
@@ -548,6 +549,7 @@ class Thread:
else:
self._daemonic = current_thread().daemon
self._ident = None
+ self._tstate_lock = None
self._started = Event()
self._stopped = False
self._block = Condition(Lock())
@@ -625,9 +627,18 @@ class Thread:
def _set_ident(self):
self._ident = get_ident()
+ def _set_tstate_lock(self):
+ """
+ Set a lock object which will be released by the interpreter when
+ the underlying thread state (see pystate.h) gets deleted.
+ """
+ self._tstate_lock = _set_sentinel()
+ self._tstate_lock.acquire()
+
def _bootstrap_inner(self):
try:
self._set_ident()
+ self._set_tstate_lock()
self._started.set()
with _active_limbo_lock:
_active[self._ident] = self
@@ -741,16 +752,17 @@ class Thread:
self._block.acquire()
try:
- if timeout is None:
- while not self._stopped:
- self._block.wait()
- else:
- deadline = _time() + timeout
- while not self._stopped:
- delay = deadline - _time()
- if delay <= 0:
- break
- self._block.wait(delay)
+ def pred():
+ if self._tstate_lock is not None:
+ # Ensure the underlying thread state has been
+ # unregistered from the interpreter (see issue #18808).
+ return self._stopped and self._tstate_lock.acquire()
+ else:
+ # Main threads and dummy threads don't have a thread
+ # state lock.
+ return self._stopped
+ self._block.wait_for(pred, timeout)
+ self._tstate_lock = None
finally:
self._block.release()
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -1172,6 +1172,61 @@ yet finished.\n\
This function is meant for internal and specialized purposes only.\n\
In most applications `threading.enumerate()` should be used instead.");
+static void
+release_sentinel(void *wr)
+{
+ /* Tricky: this function is called when the current thread state
+ is being deleted. Therefore, only simple C code can safely
+ execute here. */
+ PyObject *obj = PyWeakref_GET_OBJECT(wr);
+ lockobject *lock;
+ if (obj != Py_None) {
+ assert(Py_TYPE(obj) == &Locktype);
+ lock = (lockobject *) obj;
+ if (lock->locked) {
+ PyThread_release_lock(lock->lock_lock);
+ lock->locked = 0;
+ }
+ }
+ /* Deallocating a weakref with a NULL callback only calls
+ PyObject_GC_Del(), which can't call any Python code. */
+ Py_DECREF(wr);
+}
+
+static PyObject *
+thread__set_sentinel(PyObject *self)
+{
+ PyObject *wr;
+ PyThreadState *tstate = PyThreadState_Get();
+ lockobject *lock = newlockobject();
+
+ if (tstate->on_delete != NULL) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Can't set sentinel for the current thread state");
+ return NULL;
+ }
+ if (lock == NULL)
+ return NULL;
+ /* The lock is owned by whoever called _set_sentinel(), but the weakref
+ hangs to the thread state. */
+ wr = PyWeakref_NewRef((PyObject *) lock, NULL);
+ if (wr == NULL) {
+ Py_DECREF(lock);
+ return NULL;
+ }
+ tstate->on_delete_data = (void *) wr;
+ tstate->on_delete = &release_sentinel;
+ return (PyObject *) lock;
+}
+
+PyDoc_STRVAR(_set_sentinel_doc,
+"_set_sentinel() -> lock\n\
+\n\
+Set a sentinel lock that will be released when the current thread\n\
+state is finalized (after it is untied from the interpreter).\n\
+\n\
+This is a private API for the threading module.");
+
static PyObject *
thread_stack_size(PyObject *self, PyObject *args)
{
@@ -1247,6 +1302,8 @@ static PyMethodDef thread_methods[] = {
METH_NOARGS, _count_doc},
{"stack_size", (PyCFunction)thread_stack_size,
METH_VARARGS, stack_size_doc},
+ {"_set_sentinel", (PyCFunction)thread__set_sentinel,
+ METH_NOARGS, _set_sentinel_doc},
{NULL, NULL} /* sentinel */
};
diff --git a/Python/pystate.c b/Python/pystate.c
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -208,6 +208,8 @@ new_threadstate(PyInterpreterState *inte
tstate->trash_delete_nesting = 0;
tstate->trash_delete_later = NULL;
+ tstate->on_delete = NULL;
+ tstate->on_delete_data = NULL;
if (init)
_PyThreadState_Init(tstate);
@@ -390,6 +392,9 @@ tstate_delete_common(PyThreadState *tsta
if (tstate->next)
tstate->next->prev = tstate->prev;
HEAD_UNLOCK();
+ if (tstate->on_delete != NULL) {
+ tstate->on_delete(tstate->on_delete_data);
+ }
PyMem_RawFree(tstate);
}