From 59c44f36e0a575671c0e8dab6a9de88666e481b0 Mon Sep 17 00:00:00 2001
From: Antoine Pitrou <solipsis@pitrou.net>
Date: Fri, 30 Oct 2009 17:07:08 +0000
Subject: [PATCH] Issue #7222: Make thread "reaping" more reliable so that
 reference leak-chasing test runs give sensible results. The previous method
 of reaping threads could return successfully while some Thread objects were
 still referenced. This also introduces a new private function:
 :func:hread._count().

---
 Doc/library/thread.rst   | 14 ++++++++++++++
 Lib/test/test_support.py | 31 ++++++++++++++++++-------------
 Lib/test/test_thread.py  | 27 +++++++++++++++++++++++++++
 Misc/NEWS                |  6 ++++++
 Modules/threadmodule.c   | 20 +++++++++++++++++++-
 5 files changed, 84 insertions(+), 14 deletions(-)

diff --git a/Doc/library/thread.rst b/Doc/library/thread.rst
index f8b5850aedd..ac9d1eb0303 100644
--- a/Doc/library/thread.rst
+++ b/Doc/library/thread.rst
@@ -112,6 +112,20 @@ It defines the following constant and functions:
 
    .. versionadded:: 2.5
 
+
+.. function:: _count()
+
+   Return the number of currently running Python threads, excluding the main
+   thread.  The returned number comprises all threads created through
+   :func:`start_new_thread` as well as :class:`threading.Thread`, and not
+   yet finished.
+
+   This function is meant for internal and specialized purposes only. In
+   most applications :func:`threading.enumerate()` should be used instead.
+
+   .. versionadded:: 2.7
+
+
 Lock objects have the following methods:
 
 
diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py
index bfa3c45763d..fa46be2d0cf 100644
--- a/Lib/test/test_support.py
+++ b/Lib/test/test_support.py
@@ -952,24 +952,29 @@ def run_doctest(module, verbosity=None):
 #=======================================================================
 # Threading support to prevent reporting refleaks when running regrtest.py -R
 
-def threading_setup():
-    import threading
-    return len(threading._active), len(threading._limbo)
+# NOTE: we use thread._count() rather than threading.enumerate() (or the
+# moral equivalent thereof) because a threading.Thread object is still alive
+# until its __bootstrap() method has returned, even after it has been
+# unregistered from the threading module.
+# thread._count(), on the other hand, only gets decremented *after* the
+# __bootstrap() method has returned, which gives us reliable reference counts
+# at the end of a test run.
 
-def threading_cleanup(num_active, num_limbo):
-    import threading
+def threading_setup():
+    import thread
+    return thread._count(),
+
+def threading_cleanup(nb_threads):
+    import thread
     import time
 
     _MAX_COUNT = 10
-    count = 0
-    while len(threading._active) != num_active and count < _MAX_COUNT:
-        count += 1
-        time.sleep(0.1)
-
-    count = 0
-    while len(threading._limbo) != num_limbo and count < _MAX_COUNT:
-        count += 1
+    for count in range(_MAX_COUNT):
+        n = thread._count()
+        if n == nb_threads:
+            break
         time.sleep(0.1)
+    # XXX print a warning in case of failure?
 
 def reap_threads(func):
     @functools.wraps(func)
diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py
index 66ad22f25ad..4945047f6dd 100644
--- a/Lib/test/test_thread.py
+++ b/Lib/test/test_thread.py
@@ -4,6 +4,7 @@ import random
 from test import test_support
 import thread
 import time
+import weakref
 
 
 NUMTASKS = 10
@@ -101,6 +102,32 @@ class ThreadRunningTests(BasicThreadTest):
 
             thread.stack_size(0)
 
+    def test__count(self):
+        # Test the _count() function.
+        orig = thread._count()
+        mut = thread.allocate_lock()
+        mut.acquire()
+        started = []
+        def task():
+            started.append(None)
+            mut.acquire()
+            mut.release()
+        thread.start_new_thread(task, ())
+        while not started:
+            time.sleep(0.01)
+        self.assertEquals(thread._count(), orig + 1)
+        # Allow the task to finish.
+        mut.release()
+        # The only reliable way to be sure that the thread ended from the
+        # interpreter's point of view is to wait for the function object to be
+        # destroyed.
+        done = []
+        wr = weakref.ref(task, lambda _: done.append(None))
+        del task
+        while not done:
+            time.sleep(0.01)
+        self.assertEquals(thread._count(), orig)
+
 
 class Barrier:
     def __init__(self, num_threads):
diff --git a/Misc/NEWS b/Misc/NEWS
index 85fdc21ae67..4d6225dd521 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -1525,6 +1525,12 @@ Extension Modules
 Tests
 -----
 
+- Issue #7222: Make thread "reaping" more reliable so that reference
+  leak-chasing test runs give sensible results. The previous method of
+  reaping threads could return successfully while some Thread objects were
+  still referenced. This also introduces a new private function:
+  :func:`thread._count()`.
+
 - Issue #7151: fixed regrtest -j so that output to stderr from a test no
   longer runs the risk of causing the worker thread to fail.
 
diff --git a/Modules/threadmodule.c b/Modules/threadmodule.c
index f6d7ee4f56e..c962c79f854 100644
--- a/Modules/threadmodule.c
+++ b/Modules/threadmodule.c
@@ -14,7 +14,7 @@
 #include "pythread.h"
 
 static PyObject *ThreadError;
-
+static long nb_threads = 0;
 
 /* Lock objects */
 
@@ -439,6 +439,7 @@ t_bootstrap(void *boot_raw)
 	tstate = PyThreadState_New(boot->interp);
 
 	PyEval_AcquireThread(tstate);
+	nb_threads++;
 	res = PyEval_CallObjectWithKeywords(
 		boot->func, boot->args, boot->keyw);
 	if (res == NULL) {
@@ -463,6 +464,7 @@ t_bootstrap(void *boot_raw)
 	Py_DECREF(boot->args);
 	Py_XDECREF(boot->keyw);
 	PyMem_DEL(boot_raw);
+	nb_threads--;
 	PyThreadState_Clear(tstate);
 	PyThreadState_DeleteCurrent();
 	PyThread_exit_thread();
@@ -605,6 +607,18 @@ allocated consecutive numbers starting at 1, this behavior should not\n\
 be relied upon, and the number should be seen purely as a magic cookie.\n\
 A thread's identity may be reused for another thread after it exits.");
 
+static PyObject *
+thread__count(PyObject *self)
+{
+	return PyInt_FromLong(nb_threads);
+}
+
+PyDoc_STRVAR(_count_doc,
+"_count() -> integer\n\
+\n\
+Return the number of currently running (sub)threads.\n\
+This excludes the main thread.");
+
 static PyObject *
 thread_stack_size(PyObject *self, PyObject *args)
 {
@@ -678,6 +692,8 @@ static PyMethodDef thread_methods[] = {
 	 METH_NOARGS, interrupt_doc},
 	{"get_ident",		(PyCFunction)thread_get_ident, 
 	 METH_NOARGS, get_ident_doc},
+	{"_count",		(PyCFunction)thread__count, 
+	 METH_NOARGS, _count_doc},
 	{"stack_size",		(PyCFunction)thread_stack_size,
 				METH_VARARGS,
 				stack_size_doc},
@@ -735,6 +751,8 @@ initthread(void)
 	if (PyModule_AddObject(m, "_local", (PyObject *)&localtype) < 0)
 		return;
 
+	nb_threads = 0;
+
 	/* Initialize the C thread library */
 	PyThread_init_thread();
 }
