summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGregory P. Smith <greg@mad-scientist.com>2011-01-04 00:51:50 (GMT)
committerGregory P. Smith <greg@mad-scientist.com>2011-01-04 00:51:50 (GMT)
commit4b129d23f66f88fb91aa95d163ef55a47095ae0d (patch)
treebe89afd4f61b4dd0ca4237db17549891699ead7b
parentc3a4787ccbc0f91827120c0a519747305379e4d4 (diff)
downloadcpython-4b129d23f66f88fb91aa95d163ef55a47095ae0d.zip
cpython-4b129d23f66f88fb91aa95d163ef55a47095ae0d.tar.gz
cpython-4b129d23f66f88fb91aa95d163ef55a47095ae0d.tar.bz2
Merged revisions 87710 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/branches/py3k ........ r87710 | gregory.p.smith | 2011-01-03 13:06:12 -0800 (Mon, 03 Jan 2011) | 4 lines issue6643 - Two locks held within the threading module on each thread instance needed to be reinitialized after fork(). Adds tests to confirm that they are and that a potential deadlock and crasher bug are fixed (platform dependant). ........
-rw-r--r--Lib/test/test_threading.py152
-rw-r--r--Lib/threading.py4
-rw-r--r--Misc/NEWS3
3 files changed, 155 insertions, 4 deletions
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index 587004a..2bf8437 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -2,6 +2,7 @@
import test.support
from test.support import verbose
+import os
import random
import re
import sys
@@ -10,6 +11,7 @@ import _thread
import time
import unittest
import weakref
+import subprocess
from test import lock_tests
@@ -247,7 +249,6 @@ class ThreadTests(unittest.TestCase):
print("test_finalize_with_runnning_thread can't import ctypes")
return # can't do anything
- import subprocess
rc = subprocess.call([sys.executable, "-c", """if 1:
import ctypes, sys, time, _thread
@@ -278,7 +279,6 @@ class ThreadTests(unittest.TestCase):
def test_finalize_with_trace(self):
# Issue1733757
# Avoid a deadlock when sys.settrace steps into threading._shutdown
- import subprocess
p = subprocess.Popen([sys.executable, "-c", """if 1:
import sys, threading
@@ -311,7 +311,6 @@ class ThreadTests(unittest.TestCase):
def test_join_nondaemon_on_shutdown(self):
# Issue 1722344
# Raising SystemExit skipped threading._shutdown
- import subprocess
p = subprocess.Popen([sys.executable, "-c", """if 1:
import threading
from time import sleep
@@ -412,7 +411,6 @@ class ThreadJoinOnShutdown(unittest.TestCase):
sys.stdout.flush()
\n""" + script
- import subprocess
p = subprocess.Popen([sys.executable, "-c", script], stdout=subprocess.PIPE)
rc = p.wait()
data = p.stdout.read().decode().replace('\r', '')
@@ -484,6 +482,152 @@ class ThreadJoinOnShutdown(unittest.TestCase):
"""
self._run_and_join(script)
+ def assertScriptHasOutput(self, script, expected_output):
+ p = subprocess.Popen([sys.executable, "-c", script],
+ stdout=subprocess.PIPE)
+ rc = p.wait()
+ data = p.stdout.read().decode().replace('\r', '')
+ self.assertEqual(rc, 0, "Unexpected error")
+ self.assertEqual(data, expected_output)
+
+ @unittest.skipUnless(hasattr(os, 'fork'), "needs os.fork()")
+ def test_4_joining_across_fork_in_worker_thread(self):
+ # There used to be a possible deadlock when forking from a child
+ # thread. See http://bugs.python.org/issue6643.
+
+ # Skip platforms with known problems forking from a worker thread.
+ # See http://bugs.python.org/issue3863.
+ if sys.platform in ('freebsd4', 'freebsd5', 'freebsd6', 'os2emx'):
+ raise unittest.SkipTest('due to known OS bugs on ' + sys.platform)
+
+ # The script takes the following steps:
+ # - The main thread in the parent process starts a new thread and then
+ # tries to join it.
+ # - The join operation acquires the Lock inside the thread's _block
+ # Condition. (See threading.py:Thread.join().)
+ # - We stub out the acquire method on the condition to force it to wait
+ # until the child thread forks. (See LOCK ACQUIRED HERE)
+ # - The child thread forks. (See LOCK HELD and WORKER THREAD FORKS
+ # HERE)
+ # - The main thread of the parent process enters Condition.wait(),
+ # which releases the lock on the child thread.
+ # - The child process returns. Without the necessary fix, when the
+ # main thread of the child process (which used to be the child thread
+ # in the parent process) attempts to exit, it will try to acquire the
+ # lock in the Thread._block Condition object and hang, because the
+ # lock was held across the fork.
+
+ script = """if 1:
+ import os, time, threading
+
+ finish_join = False
+ start_fork = False
+
+ def worker():
+ # Wait until this thread's lock is acquired before forking to
+ # create the deadlock.
+ global finish_join
+ while not start_fork:
+ time.sleep(0.01)
+ # LOCK HELD: Main thread holds lock across this call.
+ childpid = os.fork()
+ finish_join = True
+ if childpid != 0:
+ # Parent process just waits for child.
+ os.waitpid(childpid, 0)
+ # Child process should just return.
+
+ w = threading.Thread(target=worker)
+
+ # Stub out the private condition variable's lock acquire method.
+ # This acquires the lock and then waits until the child has forked
+ # before returning, which will release the lock soon after. If
+ # someone else tries to fix this test case by acquiring this lock
+ # before forking instead of reseting it, the test case will
+ # deadlock when it shouldn't.
+ condition = w._block
+ orig_acquire = condition.acquire
+ call_count_lock = threading.Lock()
+ call_count = 0
+ def my_acquire():
+ global call_count
+ global start_fork
+ orig_acquire() # LOCK ACQUIRED HERE
+ start_fork = True
+ if call_count == 0:
+ while not finish_join:
+ time.sleep(0.01) # WORKER THREAD FORKS HERE
+ with call_count_lock:
+ call_count += 1
+ condition.acquire = my_acquire
+
+ w.start()
+ w.join()
+ print('end of main')
+ """
+ self.assertScriptHasOutput(script, "end of main\n")
+
+ @unittest.skipUnless(hasattr(os, 'fork'), "needs os.fork()")
+ def test_5_clear_waiter_locks_to_avoid_crash(self):
+ # Check that a spawned thread that forks doesn't segfault on certain
+ # platforms, namely OS X. This used to happen if there was a waiter
+ # lock in the thread's condition variable's waiters list. Even though
+ # we know the lock will be held across the fork, it is not safe to
+ # release locks held across forks on all platforms, so releasing the
+ # waiter lock caused a segfault on OS X. Furthermore, since locks on
+ # OS X are (as of this writing) implemented with a mutex + condition
+ # variable instead of a semaphore, while we know that the Python-level
+ # lock will be acquired, we can't know if the internal mutex will be
+ # acquired at the time of the fork.
+
+ # Skip platforms with known problems forking from a worker thread.
+ # See http://bugs.python.org/issue3863.
+ if sys.platform in ('freebsd4', 'freebsd5', 'freebsd6', 'os2emx'):
+ raise unittest.SkipTest('due to known OS bugs on ' + sys.platform)
+ script = """if True:
+ import os, time, threading
+
+ start_fork = False
+
+ def worker():
+ # Wait until the main thread has attempted to join this thread
+ # before continuing.
+ while not start_fork:
+ time.sleep(0.01)
+ childpid = os.fork()
+ if childpid != 0:
+ # Parent process just waits for child.
+ (cpid, rc) = os.waitpid(childpid, 0)
+ assert cpid == childpid
+ assert rc == 0
+ print('end of worker thread')
+ else:
+ # Child process should just return.
+ pass
+
+ w = threading.Thread(target=worker)
+
+ # Stub out the private condition variable's _release_save method.
+ # This releases the condition's lock and flips the global that
+ # causes the worker to fork. At this point, the problematic waiter
+ # lock has been acquired once by the waiter and has been put onto
+ # the waiters list.
+ condition = w._block
+ orig_release_save = condition._release_save
+ def my_release_save():
+ global start_fork
+ orig_release_save()
+ # Waiter lock held here, condition lock released.
+ start_fork = True
+ condition._release_save = my_release_save
+
+ w.start()
+ w.join()
+ print('end of main thread')
+ """
+ output = "end of worker thread\nend of main thread\n"
+ self.assertScriptHasOutput(script, output)
+
class ThreadingExceptionTests(unittest.TestCase):
# A RuntimeError should be raised if Thread.start() is called
diff --git a/Lib/threading.py b/Lib/threading.py
index 24808d4..76c729b 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -856,6 +856,10 @@ def _after_fork():
# its new value since it can have changed.
ident = _get_ident()
thread._ident = ident
+ # Any condition variables hanging off of the active thread may
+ # be in an invalid state, so we reinitialize them.
+ thread._block.__init__()
+ thread._started._cond.__init__()
new_active[ident] = thread
else:
# All the others are already stopped.
diff --git a/Misc/NEWS b/Misc/NEWS
index 7784232..1064005 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -27,6 +27,9 @@ Core and Builtins
Library
-------
+- Issue #6643: Reinitialize locks held within the threading module after fork
+ to avoid a potential rare deadlock or crash on some platforms.
+
- Issue #10806, issue #9905: Fix subprocess pipes when some of the standard
file descriptors (0, 1, 2) are closed in the parent process. Initial
patch by Ross Lagerwall.