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
55 changes: 38 additions & 17 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,6 @@ def f():
tid = _thread.start_new_thread(f, ())
done.wait()
self.assertEqual(ident[0], tid)
# Kill the "immortal" _DummyThread
del threading._active[ident[0]]

# run with a small(ish) thread stack size (256 KiB)
def test_various_ops_small_stack(self):
Expand Down Expand Up @@ -256,32 +254,55 @@ def test_various_ops_large_stack(self):

def test_foreign_thread(self):
# Check that a "foreign" thread can use the threading module.
dummy_thread = None
error = None
def f(mutex):
# Calling current_thread() forces an entry for the foreign
# thread to get made in the threading._active map.
threading.current_thread()
mutex.release()
try:
nonlocal dummy_thread
nonlocal error
# Calling current_thread() forces an entry for the foreign
# thread to get made in the threading._active map.
dummy_thread = threading.current_thread()
tid = dummy_thread.ident
self.assertIn(tid, threading._active)
self.assertIsInstance(dummy_thread, threading._DummyThread)
self.assertIs(threading._active.get(tid), dummy_thread)
# gh-29376
self.assertTrue(
dummy_thread.is_alive(),
'Expected _DummyThread to be considered alive.'
)
self.assertIn('_DummyThread', repr(dummy_thread))
except BaseException as e:
error = e
finally:
mutex.release()

mutex = threading.Lock()
mutex.acquire()
with threading_helper.wait_threads_exit():
tid = _thread.start_new_thread(f, (mutex,))
# Wait for the thread to finish.
mutex.acquire()
self.assertIn(tid, threading._active)
self.assertIsInstance(threading._active[tid], threading._DummyThread)
#Issue 29376
self.assertTrue(threading._active[tid].is_alive())
self.assertRegex(repr(threading._active[tid]), '_DummyThread')

if error is not None:
raise error
self.assertEqual(tid, dummy_thread.ident)
# Issue gh-106236:
with self.assertRaises(RuntimeError):
threading._active[tid].join()
threading._active[tid]._started.clear()
dummy_thread.join()
dummy_thread._started.clear()
with self.assertRaises(RuntimeError):
threading._active[tid].is_alive()

del threading._active[tid]
dummy_thread.is_alive()
# Busy wait for the following condition: after the thread dies, the
Comment thread
serhiy-storchaka marked this conversation as resolved.
# related dummy thread must be removed from threading._active.
timeout = 5
timeout_at = time.monotonic() + timeout
while time.monotonic() < timeout_at:
if threading._active.get(dummy_thread.ident) is not dummy_thread:
break
time.sleep(.1)
else:
self.fail('It was expected that the created threading._DummyThread was removed from threading._active.')

# PyThreadState_SetAsyncExc() is a CPython-only gimmick, not (currently)
# exposed at the Python level. This test relies on ctypes to get at it.
Expand Down
48 changes: 37 additions & 11 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@
TIMEOUT_MAX = _thread.TIMEOUT_MAX
del _thread

# get thread-local implementation, either from the thread
# module, or from the python fallback

try:
from _thread import _local as local
except ImportError:
from _threading_local import local

# Support for profile and trace hooks

Expand Down Expand Up @@ -1476,10 +1483,36 @@ def __init__(self):
_active[self._ident] = self


# Helper thread-local instance to detect when a _DummyThread
# is collected. Not a part of the public API.
_thread_local_info = local()

Comment thread
fabioz marked this conversation as resolved.

class _DeleteDummyThreadOnDel:
'''
Helper class to remove a dummy thread from threading._active on __del__.
'''

def __init__(self, dummy_thread):
self._dummy_thread = dummy_thread
self._tident = dummy_thread.ident
# Put the thread on a thread local variable so that when
# the related thread finishes this instance is collected.
#
# Note: no other references to this instance may be created.
# If any client code creates a reference to this instance,
# the related _DummyThread will be kept forever!
_thread_local_info._track_dummy_thread_ref = self

def __del__(self):
with _active_limbo_lock:
if _active.get(self._tident) is self._dummy_thread:
_active.pop(self._tident, None)


# Dummy thread class to represent threads not started here.
# These aren't garbage collected when they die, nor can they be waited for.
# If they invoke anything in threading.py that calls current_thread(), they
# leave an entry in the _active dict forever after.
# These should be added to `_active` and removed automatically
# when they die, although they can't be waited for.
# Their purpose is to return *something* from current_thread().
# They are marked as daemon threads so we won't wait for them
# when we exit (conform previous semantics).
Expand All @@ -1495,6 +1528,7 @@ def __init__(self):
self._set_native_id()
with _active_limbo_lock:
_active[self._ident] = self
_DeleteDummyThreadOnDel(self)

def _stop(self):
pass
Expand Down Expand Up @@ -1676,14 +1710,6 @@ def main_thread():
# XXX Figure this out for subinterpreters. (See gh-75698.)
return _main_thread

# get thread-local implementation, either from the thread
# module, or from the python fallback

try:
from _thread import _local as local
except ImportError:
from _threading_local import local


def _after_fork():
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``_DummyThread`` entries in ``threading._active`` are now automatically removed when the related thread dies.