Commit 85ae2a11 authored by Jason Madden's avatar Jason Madden

Progress on cross-thread locks.

Tests for semaphore are passing in both pure and compiled code. gevent.tests.test__lock.TestLockMultiThread still exposes a race condition though.
parent aa16985a
Make gevent locks that are monkey-patched work across native
threads as well as across greenlets within a single thread. This
is expensive, and not a recommended programming pattern, but it can
happen when using the threadpool. Locks that are only used in a single
thread do not take a performance hit.
The underlying Semaphore always behaves in an atomic fashion (as if
the GIL was not released) when PURE_PYTHON is set. Previously, it only
correctly did so on PyPy.
This diff is collapsed.
......@@ -554,6 +554,15 @@ class AsyncMixin(object):
def send(self):
raise NotImplementedError()
def send_ignoring_arg(self, _ignored):
"""
Calling compatibility with ``greenlet.switch(arg)``
as used by waiters that have ``rawlink``.
This is an advanced method, not usually needed.
"""
return self.send()
@property
def pending(self):
raise NotImplementedError()
......
......@@ -48,6 +48,8 @@ cdef class AbstractLinkable(object):
cpdef unlink(self, callback)
cdef _check_and_notify(self)
cdef __wait_to_be_notified(self, bint rawlink)
cdef __unlink_all(self, obj)
@cython.nonecheck(False)
cdef _notify_link_list(self, list links)
......@@ -55,6 +57,9 @@ cdef class AbstractLinkable(object):
@cython.nonecheck(False)
cpdef _notify_links(self, list arrived_while_waiting)
cpdef _drop_lock_for_switch_out(self)
cpdef _acquire_lock_for_switch_in(self)
cdef _wait_core(self, timeout, catch=*)
cdef _wait_return_value(self, waited, wait_success)
cpdef _wait(self, timeout=*)
cdef _wait(self, timeout=*)
......@@ -326,6 +326,9 @@ class AsyncResult(AbstractLinkable): # pylint:disable=undefined-variable
# Not ready and not blocking, so immediately timeout
raise Timeout()
if self.hub is None: # pylint:disable=access-member-before-definition
self.hub = get_hub() # pylint:disable=attribute-defined-outside-init
# Wait, raising a timeout that elapses
self._wait_core(timeout, ())
......
......@@ -1199,6 +1199,9 @@ cdef public class async_(watcher) [object PyGeventAsyncObject, type PyGeventAsyn
_check_loop(self.loop)
libev.ev_async_send(self.loop._ptr, &self._watcher)
def send_ignoring_arg(self, _ignored):
return self.send()
async = async_
cdef start_and_stop child_ss = make_ss(<void*>libev.ev_child_start, <void*>libev.ev_child_stop)
......
......@@ -28,11 +28,14 @@ __all__ = [
# On PyPy, we don't compile the Semaphore class with Cython. Under
# Cython, each individual method holds the GIL for its entire
# duration, ensuring that no other thread can interrupt us in an
# unsafe state (only when we _do_wait do we call back into Python and
# allow switching threads). Simulate that here through the use of a manual
# lock. (We use a separate lock for each semaphore to allow sys.settrace functions
# to use locks *other* than the one being traced.) This, of course, must also
# hold for PURE_PYTHON mode when no optional C extensions are used.
# unsafe state (only when we _wait do we call back into Python and
# allow switching threads; this is broken down into the
# _drop_lock_for_switch_out and _acquire_lock_for_switch_in methods).
# Simulate that here through the use of a manual lock. (We use a
# separate lock for each semaphore to allow sys.settrace functions to
# use locks *other* than the one being traced.) This, of course, must
# also hold for PURE_PYTHON mode when no optional C extensions are
# used.
_allocate_lock, _get_ident = monkey.get_original(
('_thread', 'thread'),
......@@ -47,15 +50,17 @@ class _OwnedLock(object):
'_locking',
'_count',
)
# Don't allow re-entry to these functions in a single thread, as can
# happen if a sys.settrace is used.
#
# This is essentially a variant of the (pure-Python) RLock from the
# standard library.
def __init__(self):
self._owner = None
self._block = _allocate_lock()
self._locking = {}
self._count = 0
# Don't allow re-entry to these functions in a single thread, as can
# happen if a sys.settrace is used.
def __begin(self):
# Return (me, count) if we should proceed, otherwise return
......@@ -89,8 +94,8 @@ class _OwnedLock(object):
self._count += 1
return
self._owner = me
self._block.acquire()
self._owner = me
self._count = 1
finally:
self.__end(me, lock_count)
......@@ -108,13 +113,13 @@ class _OwnedLock(object):
self._count = count = self._count - 1
if not count:
self._block.release()
self._owner = None
self._block.release()
finally:
self.__end(me, lock_count)
class _AtomicSemaphore(Semaphore):
class _AtomicSemaphoreMixin(object):
# Behaves as though the GIL was held for the duration of acquire, wait,
# and release, just as if we were in Cython.
#
......@@ -123,39 +128,48 @@ class _AtomicSemaphore(Semaphore):
# and re-acquire it for them on exit.
#
# Note that this does *NOT* make semaphores safe to use from multiple threads
__slots__ = (
'_lock_lock',
)
def __init__(self, *args, **kwargs):
self._lock_lock = _OwnedLock()
super(_AtomicSemaphoreMixin, self).__init__(*args, **kwargs)
super(_AtomicSemaphore, self).__init__(*args, **kwargs)
def _acquire_lock_for_switch_in(self):
self._lock_lock.acquire()
def _wait(self, *args, **kwargs):
def _drop_lock_for_switch_out(self):
self._lock_lock.release()
try:
return super(_AtomicSemaphore, self)._wait(*args, **kwargs)
finally:
self._lock_lock.acquire()
def _notify_links(self, arrived_while_waiting):
with self._lock_lock:
return super(_AtomicSemaphoreMixin, self)._notify_links(arrived_while_waiting)
def release(self):
with self._lock_lock:
return super(_AtomicSemaphore, self).release()
return super(_AtomicSemaphoreMixin, self).release()
def acquire(self, blocking=True, timeout=None):
with self._lock_lock:
return super(_AtomicSemaphore, self).acquire(blocking, timeout)
return super(_AtomicSemaphoreMixin, self).acquire(blocking, timeout)
_py3k_acquire = acquire
def wait(self, timeout=None):
with self._lock_lock:
return super(_AtomicSemaphore, self).wait(timeout)
return super(_AtomicSemaphoreMixin, self).wait(timeout)
class _AtomicSemaphore(_AtomicSemaphoreMixin, Semaphore):
__slots__ = (
'_lock_lock',
)
class _AtomicBoundedSemaphore(_AtomicSemaphoreMixin, BoundedSemaphore):
__slots__ = (
'_lock_lock',
)
if PURE_PYTHON:
Semaphore = _AtomicSemaphore
BoundedSemaphore = _AtomicBoundedSemaphore
class DummySemaphore(object):
......
......@@ -5,7 +5,13 @@ from __future__ import print_function
from gevent import lock
import gevent.testing as greentest
from gevent.tests import test__semaphore
class TestLockMultiThread(test__semaphore.TestSemaphoreMultiThread):
def _makeOne(self):
return lock.RLock()
if __name__ == '__main__':
greentest.main()
......@@ -14,7 +14,7 @@ import gevent.exceptions
from gevent.lock import Semaphore
import gevent.testing as greentest
from gevent.testing import timing
class TestSemaphore(greentest.TestCase):
......@@ -67,7 +67,7 @@ class TestSemaphore(greentest.TestCase):
gevent.wait([s])
class TestAcquireContended(greentest.TestCase):
class TestSemaphoreMultiThread(greentest.TestCase):
# Tests that the object can be acquired correctly across
# multiple threads.
# Used as a base class.
......@@ -81,39 +81,113 @@ class TestAcquireContended(greentest.TestCase):
# would be from an arbitrary thread.
return Semaphore(1, gevent.get_hub())
def test_acquire_in_one_then_another(self):
def test_acquire_in_one_then_another(self, release=True, **thread_acquire_kwargs):
from gevent import monkey
self.assertFalse(monkey.is_module_patched('threading'))
import sys
import threading
thread_running = threading.Event()
thread_acquired = threading.Event()
sem = self._makeOne()
# Make future acquires block
print("acquiring", sem)
sem.acquire()
exc_info = []
acquired = []
def thread_main():
# XXX: When this is fixed, this will have to be modified
# to avoid deadlock, but being careful to still test
# the initial conditions (e.g., that this doesn't throw;
# we can't pass block=False because that bypasses the part
# that would throw.)
thread_running.set()
try:
sem.acquire()
acquired.append(
sem.acquire(**thread_acquire_kwargs)
)
except:
exc_info[:] = sys.exc_info()
raise # Print
finally:
thread_acquired.set()
t = threading.Thread(target=thread_main)
t.start()
t.join()
while not thread_running.is_set():
thread_running.wait(timing.LARGE_TICK * 5)
if release:
sem.release()
# Spin the loop to be sure the release gets through.
gevent.idle()
thread_acquired.wait(timing.LARGE_TICK * 5)
self.assertEqual(acquired, [True])
thread_acquired.wait(timing.LARGE_TICK * 5)
try:
self.assertEqual(exc_info, [])
finally:
exc_info = None
return sem, acquired
def test_acquire_in_one_then_another_timed(self):
sem, acquired_in_thread = self.test_acquire_in_one_then_another(
release=False,
timeout=timing.SMALLEST_RELIABLE_DELAY)
self.assertEqual([False], acquired_in_thread)
# This doesn't, of course, notify anything, because
# the waiter has given up.
sem.release()
notifier = getattr(sem, '_notifier', None)
self.assertIsNone(notifier)
def test_acquire_in_one_wait_greenlet_wait_thread_gives_up(self):
# The waiter in the thread both arrives and gives up while
# the notifier is already running...or at least, that's what
# we'd like to arrange, but the _notify_links function doesn't
# drop the GIL/object lock, so the other thread is stuck and doesn't
# actually get to call into the acquire method.
from gevent import monkey
self.assertFalse(monkey.is_module_patched('threading'))
import sys
import threading
sem = self._makeOne()
# Make future acquires block
sem.acquire()
def greenlet_one():
ack = sem.acquire()
# We're running in the notifier function right now. It switched to
# us.
thread.start()
gevent.sleep(timing.LARGE_TICK)
return ack
exc_info = []
acquired = []
def thread_main():
try:
acquired.append(
sem.acquire(timeout=timing.LARGE_TICK)
)
except:
exc_info[:] = sys.exc_info()
raise # Print
glet = gevent.spawn(greenlet_one)
thread = threading.Thread(target=thread_main)
gevent.idle()
sem.release()
glet.join()
thread.join(timing.LARGE_TICK)
self.assertEqual(glet.value, True)
self.assertEqual([], exc_info)
self.assertEqual([False], acquired)
# XXX: Need a test with multiple greenlets in a non-primary
# thread. Things should work, just very slowly; instead of moving through
# greenlet.switch(), they'll be moving with async watchers.
@greentest.skipOnPurePython("Needs C extension")
class TestCExt(greentest.TestCase):
......
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