Commit 06ede5d7 authored by Jason Madden's avatar Jason Madden

Fix #1464 and fix #1465 by making LockType sleep on failure to non-blocking acquire the lock.

Using sleep doesn't force a trip around the event loop until the switch interval elapses.
parent 421bb697
...@@ -29,6 +29,12 @@ ...@@ -29,6 +29,12 @@
greenlets. Previously this was hidden by forcing buffering, which greenlets. Previously this was hidden by forcing buffering, which
raised ``RuntimeError``. raised ``RuntimeError``.
- Fix using monkey-patched ``threading.Lock`` and ``threading.RLock``
objects as spin locks by making them call ``sleep(0)`` if they
failed to acquire the lock in a non-blocking call. This lets other
callbacks run to release the lock, simulating preemptive threading.
Using spin locks is not recommended, but may have been done in code
written for threads, especially on Python 3. See :issue:`1464`.
1.5a2 (2019-10-21) 1.5a2 (2019-10-21)
================== ==================
......
...@@ -498,6 +498,16 @@ def patch_time(): ...@@ -498,6 +498,16 @@ def patch_time():
def _patch_existing_locks(threading): def _patch_existing_locks(threading):
if len(list(threading.enumerate())) != 1: if len(list(threading.enumerate())) != 1:
return return
# This is used to protect internal data structures for enumerate.
# It's acquired when threads are started and when they're stopped.
# Stopping a thread checks a Condition, which on Python 2 wants to test
# _is_owned of its (patched) Lock. Since our LockType doesn't have
# _is_owned, it tries to acquire the lock non-blocking; that triggers a
# switch. If the next thing in the callback list was a thread that needed
# to start or end, we wouldn't be able to acquire this native lock
# because it was being held already; we couldn't switch either, so we'd
# block permanently.
threading._active_limbo_lock = threading._allocate_lock()
try: try:
tid = threading.get_ident() tid = threading.get_ident()
except AttributeError: except AttributeError:
...@@ -566,6 +576,7 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True ...@@ -566,6 +576,7 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True
instances that are currently locked can be properly unlocked. **Important**: This is a instances that are currently locked can be properly unlocked. **Important**: This is a
best-effort attempt and, on certain implementations, may not detect all best-effort attempt and, on certain implementations, may not detect all
locks. It is important to monkey-patch extremely early in the startup process. locks. It is important to monkey-patch extremely early in the startup process.
Setting this to False is not recommended, especially on Python 2.
.. caution:: .. caution::
Monkey-patching :mod:`thread` and using Monkey-patching :mod:`thread` and using
......
"""
Tests specifically for the monkey-patched threading module.
"""
from gevent import monkey; monkey.patch_all() from gevent import monkey; monkey.patch_all()
import gevent.hub import gevent.hub
...@@ -14,7 +17,7 @@ def helper(): ...@@ -14,7 +17,7 @@ def helper():
gevent.sleep(0.2) gevent.sleep(0.2)
class Test(greentest.TestCase): class TestCleanup(greentest.TestCase):
def _do_test(self, spawn): def _do_test(self, spawn):
before = len(threading._active) before = len(threading._active)
...@@ -46,5 +49,44 @@ class Test(greentest.TestCase): ...@@ -46,5 +49,44 @@ class Test(greentest.TestCase):
def test_cleanup_raw(self): def test_cleanup_raw(self):
self._do_test(gevent.spawn_raw) self._do_test(gevent.spawn_raw)
class TestLockThread(greentest.TestCase):
def _spawn(self, func):
t = threading.Thread(target=func)
t.start()
return t
def test_spin_lock_switches(self):
# https://github.com/gevent/gevent/issues/1464
lock = threading.Lock()
lock.acquire()
spawned = []
def background():
spawned.append(True)
while 1:
# blocking= in Py3, wait (no default, no name) in Py2
if lock.acquire(False):
break
thread = threading.Thread(target=background)
# If lock.acquire(False) doesn't yield when it fails,
# then this never returns.
thread.start()
# Verify it tried to run
self.assertEqual(spawned, [True])
# We can attempt to join it, which won't work.
thread.join(0)
# We can release the lock and then it will acquire.
lock.release()
thread.join()
class TestLockGreenlet(TestLockThread):
def _spawn(self, func):
return gevent.spawn(func)
if __name__ == '__main__': if __name__ == '__main__':
greentest.main() greentest.main()
...@@ -82,7 +82,6 @@ class TestTrace(unittest.TestCase): ...@@ -82,7 +82,6 @@ class TestTrace(unittest.TestCase):
else: else:
old = None old = None
PY3 = sys.version_info[0] > 2
lst = [] lst = []
# we should be able to use unrelated locks from within the trace function # we should be able to use unrelated locks from within the trace function
l = allocate_lock() l = allocate_lock()
...@@ -90,7 +89,7 @@ class TestTrace(unittest.TestCase): ...@@ -90,7 +89,7 @@ class TestTrace(unittest.TestCase):
def trace(frame, ev, _arg): def trace(frame, ev, _arg):
with l: with l:
lst.append((frame.f_code.co_filename, frame.f_lineno, ev)) lst.append((frame.f_code.co_filename, frame.f_lineno, ev))
print("TRACE: %s:%s %s" % lst[-1]) # print("TRACE: %s:%s %s" % lst[-1])
return trace return trace
l2 = allocate_lock() l2 = allocate_lock()
...@@ -102,12 +101,8 @@ class TestTrace(unittest.TestCase): ...@@ -102,12 +101,8 @@ class TestTrace(unittest.TestCase):
finally: finally:
sys.settrace(old) sys.settrace(old)
if not PY3: # Have an assert so that we know if we miscompile
# Py3 overrides acquire in Python to do argument checking self.assertTrue(lst, "should not compile on pypy")
self.assertEqual(lst, [], "trace not empty")
else:
# Have an assert so that we know if we miscompile
self.assertTrue(lst, "should not compile on pypy")
@greentest.skipOnPurePython("Locks can be traced in Pure Python") @greentest.skipOnPurePython("Locks can be traced in Pure Python")
def test_untraceable_lock_uses_same_lock(self): def test_untraceable_lock_uses_same_lock(self):
...@@ -116,7 +111,7 @@ class TestTrace(unittest.TestCase): ...@@ -116,7 +111,7 @@ class TestTrace(unittest.TestCase):
old = sys.gettrace() old = sys.gettrace()
else: else:
old = None old = None
PY3 = sys.version_info[0] > 2
lst = [] lst = []
e = None e = None
# we should not be able to use the same lock from within the trace function # we should not be able to use the same lock from within the trace function
...@@ -137,13 +132,9 @@ class TestTrace(unittest.TestCase): ...@@ -137,13 +132,9 @@ class TestTrace(unittest.TestCase):
finally: finally:
sys.settrace(old) sys.settrace(old)
if not PY3: # Have an assert so that we know if we miscompile
# Py3 overrides acquire in Python to do argument checking self.assertTrue(lst, "should not compile on pypy")
self.assertEqual(lst, [], "trace not empty") self.assertTrue(isinstance(e, LoopExit))
else:
# Have an assert so that we know if we miscompile
self.assertTrue(lst, "should not compile on pypy")
self.assertTrue(isinstance(e, LoopExit))
def run_script(self, more_args=()): def run_script(self, more_args=()):
args = [sys.executable, "-c", script] args = [sys.executable, "-c", script]
......
...@@ -53,7 +53,10 @@ error = __thread__.error ...@@ -53,7 +53,10 @@ error = __thread__.error
from gevent._compat import PYPY from gevent._compat import PYPY
from gevent._util import copy_globals from gevent._util import copy_globals
from gevent.hub import getcurrent, GreenletExit from gevent.hub import getcurrent
from gevent.hub import GreenletExit
from gevent.hub import sleep
from gevent._hub_local import get_hub_if_exists
from gevent.greenlet import Greenlet from gevent.greenlet import Greenlet
from gevent.lock import BoundedSemaphore from gevent.lock import BoundedSemaphore
from gevent.local import local as _local from gevent.local import local as _local
...@@ -89,23 +92,45 @@ class LockType(BoundedSemaphore): ...@@ -89,23 +92,45 @@ class LockType(BoundedSemaphore):
if PY3: if PY3:
_TIMEOUT_MAX = __thread__.TIMEOUT_MAX # python 2: pylint:disable=no-member _TIMEOUT_MAX = __thread__.TIMEOUT_MAX # python 2: pylint:disable=no-member
else:
def acquire(self, blocking=True, timeout=-1): _TIMEOUT_MAX = 9223372036.0
# Transform the default -1 argument into the None that our
# semaphore implementation expects, and raise the same error def acquire(self, blocking=True, timeout=-1):
# the stdlib implementation does. # This is the Python 3 signature.
if timeout == -1: # On Python 2, Lock.acquire has the signature `Lock.acquire([wait])`
timeout = None # where `wait` is a boolean that cannot be passed by name, only position.
if not blocking and timeout is not None: # so we're fine to use the Python 3 signature.
raise ValueError("can't specify a timeout for a non-blocking call")
if timeout is not None: # Transform the default -1 argument into the None that our
if timeout < 0: # semaphore implementation expects, and raise the same error
# in C: if(timeout < 0 && timeout != -1) # the stdlib implementation does.
raise ValueError("timeout value must be strictly positive") if timeout == -1:
if timeout > self._TIMEOUT_MAX: timeout = None
raise OverflowError('timeout value is too large') if not blocking and timeout is not None:
raise ValueError("can't specify a timeout for a non-blocking call")
return BoundedSemaphore.acquire(self, blocking, timeout) if timeout is not None:
if timeout < 0:
# in C: if(timeout < 0 && timeout != -1)
raise ValueError("timeout value must be strictly positive")
if timeout > self._TIMEOUT_MAX:
raise OverflowError('timeout value is too large')
acquired = BoundedSemaphore.acquire(self, blocking, timeout)
if not acquired and not blocking and getcurrent() is not get_hub_if_exists():
# Run other callbacks. This makes spin locks works.
# We can't do this if we're in the hub, which we could easily be:
# printing the repr of a thread checks its tstate_lock, and sometimes we
# print reprs in the hub.
# See https://github.com/gevent/gevent/issues/1464
# By using sleep() instead of self.wait(0), we don't force a trip
# around the event loop *unless* we've been running callbacks for
# longer than our switch interval.
sleep()
return acquired
# Should we implement _is_owned, at least for Python 2? See notes in
# monkey.py's patch_existing_locks.
allocate_lock = LockType allocate_lock = LockType
......
...@@ -266,12 +266,16 @@ class Timeout(BaseException): ...@@ -266,12 +266,16 @@ class Timeout(BaseException):
# Internal use only in 1.1 # Internal use only in 1.1
# Return an object with a 'cancel' method; if timeout is None, # Return an object with a 'cancel' method; if timeout is None,
# this will be a shared instance object that does nothing. Otherwise, # this will be a shared instance object that does nothing. Otherwise,
# return an actual Timeout. Because negative values are hard to reason about, # return an actual Timeout. A 0 value is allowed and creates a real Timeout.
# Because negative values are hard to reason about,
# and are often used as sentinels in Python APIs, in the future it's likely # and are often used as sentinels in Python APIs, in the future it's likely
# that a negative timeout will also return the shared instance. # that a negative timeout will also return the shared instance.
# This saves the previously common idiom of 'timer = Timeout.start_new(t) if t is not None else None' # This saves the previously common idiom of
# 'timer = Timeout.start_new(t) if t is not None else None'
# followed by 'if timer is not None: timer.cancel()'. # followed by 'if timer is not None: timer.cancel()'.
# That idiom was used to avoid any object allocations. # That idiom was used to avoid any object allocations.
# A staticmethod is slightly faster under CPython, compared to a classmethod; # A staticmethod is slightly faster under CPython, compared to a classmethod;
# under PyPy in synthetic benchmarks it makes no difference. # under PyPy in synthetic benchmarks it makes no difference.
if timeout is None: if timeout is None:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment