]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.9] bpo-37788: Fix reference leak when Thread is never joined (GH-26103) (GH-26142)
authorAntoine Pitrou <antoine@python.org>
Sat, 15 May 2021 09:51:20 +0000 (11:51 +0200)
committerGitHub <noreply@github.com>
Sat, 15 May 2021 09:51:20 +0000 (02:51 -0700)
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 c10c2ec7a0e06975e8010c56c9c3270f8ea322ec)

Co-authored-by: Antoine Pitrou <antoine@python.org>
Automerge-Triggered-By: GH:pitrou
Lib/test/test_threading.py
Lib/threading.py
Misc/NEWS.d/next/Library/2021-05-13-19-07-28.bpo-37788.adeFcf.rst [new file with mode: 0644]

index c21cdf8eb7be9ca7913e186ee84eef52b569d6f6..67e061e8aa63bcb984e437bc753f804e47a1dcfa 100644 (file)
@@ -805,6 +805,14 @@ class ThreadTests(BaseTestCase):
         """)
         self.assertEqual(out.rstrip(), b"thread_dict.atexit = 'value'")
 
+    def test_leak_without_join(self):
+        # bpo-37788: Test that a thread which is not joined explicitly
+        # does not leak. Test written for reference leak checks.
+        def noop(): pass
+        with support.wait_threads_exit():
+            threading.Thread(target=noop).start()
+            # Thread.join() is not called
+
 
 class ThreadJoinOnShutdown(BaseTestCase):
 
index 4da5c657b1b73a098df5f6918075908e94362c58..702acaa0054307cf05428dc94cc86c111964a75f 100644 (file)
@@ -755,12 +755,27 @@ _active_limbo_lock = _allocate_lock()
 _active = {}    # maps thread id to Thread object
 _limbo = {}
 _dangling = WeakSet()
+
 # Set of Thread._tstate_lock locks of non-daemon threads used by _shutdown()
 # to wait until all Python thread states get deleted:
 # see Thread._set_tstate_lock().
 _shutdown_locks_lock = _allocate_lock()
 _shutdown_locks = set()
 
+def _maintain_shutdown_locks():
+    """
+    Drop any shutdown locks that don't correspond to running threads anymore.
+
+    Calling this from time to time avoids an ever-growing _shutdown_locks
+    set when Thread objects are not joined explicitly. See bpo-37788.
+
+    This must be called with _shutdown_locks_lock acquired.
+    """
+    # If a lock was released, the corresponding thread has exited
+    to_remove = [lock for lock in _shutdown_locks if not lock.locked()]
+    _shutdown_locks.difference_update(to_remove)
+
+
 # Main class for threads
 
 class Thread:
@@ -932,6 +947,7 @@ class Thread:
 
         if not self.daemon:
             with _shutdown_locks_lock:
+                _maintain_shutdown_locks()
                 _shutdown_locks.add(self._tstate_lock)
 
     def _bootstrap_inner(self):
@@ -987,7 +1003,8 @@ class Thread:
         self._tstate_lock = None
         if not self.daemon:
             with _shutdown_locks_lock:
-                _shutdown_locks.discard(lock)
+                # Remove our lock and other released locks from _shutdown_locks
+                _maintain_shutdown_locks()
 
     def _delete(self):
         "Remove current thread from the dict of currently running threads."
diff --git a/Misc/NEWS.d/next/Library/2021-05-13-19-07-28.bpo-37788.adeFcf.rst b/Misc/NEWS.d/next/Library/2021-05-13-19-07-28.bpo-37788.adeFcf.rst
new file mode 100644 (file)
index 0000000..0c33923
--- /dev/null
@@ -0,0 +1 @@
+Fix a reference leak when a Thread object is never joined.