8000 [3.9] bpo-37788: Fix reference leak when Thread is never joined (GH-2… · python/cpython@b30b25b · GitHub
[go: up one dir, main page]

Skip to content

Commit b30b25b

Browse files
authored
[3.9] bpo-37788: Fix reference leak when Thread is never joined (GH-26103) (GH-26142)
When a Thread is not joined after it has stopped, its lock may remain in the _shutdown_locks set until interpreter shutdown. If many threads are created this way, the _shutdown_locks set could therefore grow endlessly. To avoid such a situation, purge expired locks each time a new one is added or removed.. (cherry picked from commit c10c2ec) Co-authored-by: Antoine Pitrou <antoine@python.org> Automerge-Triggered-By: GH:pitrou
1 parent fa9de0c commit b30b25b

File tree

3 files changed

+27
-1
lines changed

3 files changed

+27
-1
lines changed

Lib/test/test_threading.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,14 @@ def __del__(self):
805805
""")
806806
self.assertEqual(out.rstrip(), b"thread_dict.atexit = 'value'")
807807

808+
def test_leak_without_join(self):
809+
# bpo-37788: Test that a thread which is not joined explicitly
810+
# does not leak. Test written for reference leak checks.
811+
def noop(): pass
812+
with support.wait_threads_exit():
813+
threading.Thread(target=noop).start()
814+
# Thread.join() is not called
815+
808816

809817
class ThreadJoinOnShutdown(BaseTestCase):
810818

Lib/threading.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,12 +755,27 @@ def _newname(template="Thread-%d"):
755755
_active = {} # maps thread id to Thread object
756756
_limbo = {}
757757
_dangling = WeakSet()
758+
758759
# Set of Thread._tstate_lock locks of non-daemon threads used by _shutdown()
759760
# to wait until all Python thread states get deleted:
760761
# see Thread._set_tstate_lock().
761762
_shutdown_locks_lock = _allocate_lock()
762763
_shutdown_locks = set()
763764

765+
def _maintain_shutdown_locks():
766+
"""
767+
Drop any shutdown locks that don't correspond to running threads anymore.
768+
769+
Calling this from time to time avoids an ever-growing _shutdown_locks
770+
set when Thread objects are not joined explicitly. See bpo-37788.
771+
772+
This must be called with _shutdown_locks_lock acquired.
773+
"""
774+
# If a lock was released, the corresponding thread has exited
775+
to_remove = [lock for lock in _shutdown_locks if not lock.locked()]
776+
_shutdown_locks.difference_update(to_remove)
777+
778+
764779
# Main class for threads
765780

766781
class Thread:
@@ -932,6 +947,7 @@ def _set_tstate_lock(self):
932947

933948
if not self.daemon:
934949
with _shutdown_locks_lock:
950+
_maintain_shutdown_locks()
935951
_shutdown_locks.add(self._tstate_lock)
936952

937953
def _bootstrap_inner(self):
@@ -987,7 +1003,8 @@ def _stop(self):
9871003
self._tstate_lock = None
9881004
if not self.daemon:
9891005
with _shutdown_locks_lock:
990-
_shutdown_locks.discard(lock)
1006+
# Remove our lock and other released locks from _shutdown_locks
1007+
_maintain_shutdown_locks()
9911008

9921009
def _delete(self):
9931010
"Remove current thread from the dict of currently running threads."
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a reference leak when a Thread object is never joined.

0 commit comments

Comments
 (0)
0