diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 933935ba2ce2c8..a458c463eb6195 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -874,6 +874,27 @@ def __call__(self): thread.join() self.assertTrue(target.ran) + 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 threading_helper.wait_threads_exit(): + threading.Thread(target=noop).start() + # Thread.join() is not called + + def test_leak_without_join_2(self): + # Same as above, but a delay gets introduced after the thread's + # Python code returned but before the thread state is deleted. + def random_sleep(): + seconds = random.random() * 0.010 + time.sleep(seconds) + + def f(): + random_sleep() + + with threading_helper.wait_threads_exit(): + threading.Thread(target=f).start() + # Thread.join() is not called class ThreadJoinOnShutdown(BaseTestCase): diff --git a/Lib/threading.py b/Lib/threading.py index ff2624a3e1e49e..682f7f4bb7d696 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1407,6 +1407,15 @@ def _register_atexit(func, *arg, **kwargs): _main_thread = _MainThread() +def _discard_shutdown_lock(lock): + """ + Discard the Thread._tstate_lock lock when the non-daemon thread have done. + """ + if lock not in _shutdown_locks: + return + with _shutdown_locks_lock: + _shutdown_locks.discard(lock) + def _shutdown(): """ Wait until the Python thread state of all non-daemon threads get deleted. diff --git a/Misc/NEWS.d/next/Library/2021-04-07-01-33-40.bpo-37788.F0tR05.rst b/Misc/NEWS.d/next/Library/2021-04-07-01-33-40.bpo-37788.F0tR05.rst new file mode 100644 index 00000000000000..d9b1e82b92238a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-04-07-01-33-40.bpo-37788.F0tR05.rst @@ -0,0 +1 @@ +Fix a reference leak if a thread is not joined. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 0613dfd3070c5c..3ace7ccacbe7b8 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -3,8 +3,9 @@ /* Interface to Sjoerd's portable C thread library */ #include "Python.h" -#include "pycore_pylifecycle.h" #include "pycore_interp.h" // _PyInterpreterState.num_threads +#include "pycore_pyerrors.h" // _PyErr_Occurred() +#include "pycore_pylifecycle.h" #include "pycore_pystate.h" // _PyThreadState_Init() #include // offsetof() #include "structmember.h" // PyMemberDef @@ -20,6 +21,8 @@ _Py_IDENTIFIER(__dict__); _Py_IDENTIFIER(stderr); _Py_IDENTIFIER(flush); +_Py_IDENTIFIER(threading); +_Py_IDENTIFIER(_discard_shutdown_lock); // Forward declarations @@ -1283,13 +1286,33 @@ release_sentinel(void *wr_raw) execute here. */ PyObject *obj = PyWeakref_GET_OBJECT(wr); lockobject *lock; + if (obj != Py_None) { lock = (lockobject *) obj; if (lock->locked) { PyThread_release_lock(lock->lock_lock); lock->locked = 0; } + PyThreadState *tstate = _PyThreadState_GET(); + PyObject *threading = _PyImport_GetModuleId(&PyId_threading); + if (threading == NULL) { + if (!_PyErr_Occurred(tstate)) { + _PyErr_SetString(tstate, PyExc_RuntimeError, + "lost threading module"); + } + goto exit; + } + PyObject *result = _PyObject_CallMethodIdOneArg( + threading, &PyId__discard_shutdown_lock, obj); + if (result == NULL) { + goto exit; + } else { + Py_DECREF(result); + } + Py_DECREF(threading); } + +exit: /* Deallocating a weakref with a NULL callback only calls PyObject_GC_Del(), which can't call any Python code. */ Py_DECREF(wr);