Skip to content

Commit 16edc6a

Browse files
committed
bpo-37788: Fix reference leak when Thread is never joined
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.
1 parent 366c69f commit 16edc6a

File tree

3 files changed

+26
-1
lines changed

3 files changed

+26
-1
lines changed

Lib/test/test_threading.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,13 @@ def __call__(self):
907907
thread.join()
908908
self.assertTrue(target.ran)
909909

910+
def test_leak_without_join(self):
911+
# bpo-37788: Test that a thread which is not joined explicitly
912+
# does not leak. Test written for reference leak checks.
913+
def noop(): pass
914+
with threading_helper.wait_threads_exit():
915+
threading.Thread(target=noop).start()
916+
# Thread.join() is not called
910917

911918

912919
class ThreadJoinOnShutdown(BaseTestCase):

Lib/threading.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,12 +780,27 @@ def _newname(name_template):
780780
_active = {} # maps thread id to Thread object
781781
_limbo = {}
782782
_dangling = WeakSet()
783+
783784
# Set of Thread._tstate_lock locks of non-daemon threads used by _shutdown()
784785
# to wait until all Python thread states get deleted:
785786
# see Thread._set_tstate_lock().
786787
_shutdown_locks_lock = _allocate_lock()
787788
_shutdown_locks = set()
788789

790+
def _maintain_shutdown_locks():
791+
"""
792+
Drop any shutdown locks that don't correspond to running threads anymore.
793+
794+
Calling this from time to time avoids an ever-growing _shutdown_locks
795+
set when Thread objects are not joined explicitly. See bpo-37788.
796+
797+
This must be called with _shutdown_locks_lock acquired.
798+
"""
799+
# If a lock was released, the corresponding thread has exited
800+
to_remove = [lock for lock in _shutdown_locks if not lock.locked()]
801+
_shutdown_locks.difference_update(to_remove)
802+
803+
789804
# Main class for threads
790805

791806
class Thread:
@@ -968,6 +983,7 @@ def _set_tstate_lock(self):
968983

969984
if not self.daemon:
970985
with _shutdown_locks_lock:
986+
_maintain_shutdown_locks()
971987
_shutdown_locks.add(self._tstate_lock)
972988

973989
def _bootstrap_inner(self):
@@ -1023,7 +1039,8 @@ def _stop(self):
10231039
self._tstate_lock = None
10241040
if not self.daemon:
10251041
with _shutdown_locks_lock:
1026-
_shutdown_locks.discard(lock)
1042+
# Remove our lock and other released locks from _shutdown_locks
1043+
_maintain_shutdown_locks()
10271044

10281045
def _delete(self):
10291046
"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)