diff options
author | Jesse Noller <jnoller@gmail.com> | 2008-07-16 20:03:47 (GMT) |
---|---|---|
committer | Jesse Noller <jnoller@gmail.com> | 2008-07-16 20:03:47 (GMT) |
commit | 5e62ca4fea04ad7b349330c50dc11937ab3278f4 (patch) | |
tree | 3a37b08db904d9e9239b7d08f630f24c46569a61 | |
parent | 1bbf4ea553a5089ec82377dd24531dd79e3357c6 (diff) | |
download | cpython-5e62ca4fea04ad7b349330c50dc11937ab3278f4.zip cpython-5e62ca4fea04ad7b349330c50dc11937ab3278f4.tar.gz cpython-5e62ca4fea04ad7b349330c50dc11937ab3278f4.tar.bz2 |
Apply patch for 874900: threading module can deadlock after fork
-rw-r--r-- | Lib/test/test_threading.py | 80 | ||||
-rw-r--r-- | Lib/threading.py | 31 | ||||
-rw-r--r-- | Python/ceval.c | 20 |
3 files changed, 130 insertions, 1 deletions
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index db6b0d7..8c1ecce 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -323,6 +323,82 @@ class ThreadTests(unittest.TestCase): sys.getrefcount(weak_raising_cyclic_object()))) +class ThreadJoinOnShutdown(unittest.TestCase): + + def _run_and_join(self, script): + script = """if 1: + import sys, os, time, threading + + # a thread, which waits for the main program to terminate + def joiningfunc(mainthread): + mainthread.join() + print 'end of thread' + \n""" + script + + import subprocess + p = subprocess.Popen([sys.executable, "-c", script], stdout=subprocess.PIPE) + rc = p.wait() + self.assertEqual(p.stdout.read(), "end of main\nend of thread\n") + self.failIf(rc == 2, "interpreter was blocked") + self.failUnless(rc == 0, "Unexpected error") + + def test_1_join_on_shutdown(self): + # The usual case: on exit, wait for a non-daemon thread + script = """if 1: + import os + t = threading.Thread(target=joiningfunc, + args=(threading.current_thread(),)) + t.start() + time.sleep(0.1) + print 'end of main' + """ + self._run_and_join(script) + + + def test_2_join_in_forked_process(self): + # Like the test above, but from a forked interpreter + import os + if not hasattr(os, 'fork'): + return + script = """if 1: + childpid = os.fork() + if childpid != 0: + os.waitpid(childpid, 0) + sys.exit(0) + + t = threading.Thread(target=joiningfunc, + args=(threading.current_thread(),)) + t.start() + print 'end of main' + """ + self._run_and_join(script) + + def test_3_join_in_forked_from_thread(self): + # Like the test above, but fork() was called from a worker thread + # In the forked process, the main Thread object must be marked as stopped. + import os + if not hasattr(os, 'fork'): + return + script = """if 1: + main_thread = threading.current_thread() + def worker(): + childpid = os.fork() + if childpid != 0: + os.waitpid(childpid, 0) + sys.exit(0) + + t = threading.Thread(target=joiningfunc, + args=(main_thread,)) + print 'end of main' + t.start() + t.join() # Should not block: main_thread is already stopped + + w = threading.Thread(target=worker) + w.start() + """ + self._run_and_join(script) + + class ThreadingExceptionTests(unittest.TestCase): # A RuntimeError should be raised if Thread.start() is called # multiple times. @@ -363,7 +439,9 @@ class ThreadingExceptionTests(unittest.TestCase): def test_main(): test.test_support.run_unittest(ThreadTests, - ThreadingExceptionTests) + ThreadJoinOnShutdown, + ThreadingExceptionTests, + ) if __name__ == "__main__": test_main() diff --git a/Lib/threading.py b/Lib/threading.py index bfca44c..8a1de42 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -825,6 +825,37 @@ except ImportError: from _threading_local import local +def _after_fork(): + # This function is called by Python/ceval.c:PyEval_ReInitThreads which + # is called from PyOS_AfterFork. Here we cleanup threading module state + # that should not exist after a fork. + + # Reset _active_limbo_lock, in case we forked while the lock was held + # by another (non-forked) thread. http://bugs.python.org/issue874900 + global _active_limbo_lock + _active_limbo_lock = _allocate_lock() + + # fork() only copied the current thread; clear references to others. + new_active = {} + current = current_thread() + with _active_limbo_lock: + for ident, thread in _active.iteritems(): + if thread is current: + # There is only one active thread. + new_active[ident] = thread + else: + # All the others are already stopped. + # We don't call _Thread__stop() because it tries to acquire + # thread._Thread__block which could also have been held while + # we forked. + thread._Thread__stopped = True + + _limbo.clear() + _active.clear() + _active.update(new_active) + assert len(_active) == 1 + + # Self-test code def _test(): diff --git a/Python/ceval.c b/Python/ceval.c index a9e37ae..f61bcd5 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -274,6 +274,9 @@ PyEval_ReleaseThread(PyThreadState *tstate) void PyEval_ReInitThreads(void) { + PyObject *threading, *result; + PyThreadState *tstate; + if (!interpreter_lock) return; /*XXX Can't use PyThread_free_lock here because it does too @@ -283,6 +286,23 @@ PyEval_ReInitThreads(void) interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock, 1); main_thread = PyThread_get_thread_ident(); + + /* Update the threading module with the new state. + */ + tstate = PyThreadState_GET(); + threading = PyMapping_GetItemString(tstate->interp->modules, + "threading"); + if (threading == NULL) { + /* threading not imported */ + PyErr_Clear(); + return; + } + result = PyObject_CallMethod(threading, "_after_fork", NULL); + if (result == NULL) + PyErr_WriteUnraisable(threading); + else + Py_DECREF(result); + Py_DECREF(threading); } #endif |