diff --git a/src/extrainterpreters/lock.py b/src/extrainterpreters/lock.py index dd1c256..ae46f35 100644 --- a/src/extrainterpreters/lock.py +++ b/src/extrainterpreters/lock.py @@ -45,7 +45,10 @@ class _CrossInterpreterStructLock: """ def __init__(self, struct, timeout=DEFAULT_TIMEOUT): - buffer_ptr, size = _address_and_size(struct._data) # , struct._offset) + if isinstance(struct._data, RemoteArray): + buffer_ptr, size = struct._data._data_for_remote() + else: # bytes, bytearray + buffer_ptr, size = _address_and_size(struct._data) # , struct._offset) # struct_ptr = buffer_ptr + struct._offset lock_offset = struct._offset + struct._get_offset_for_field("lock") if lock_offset >= size: @@ -105,7 +108,7 @@ def release(self): def __getstate__(self): state = self.__dict__.copy() - state["_entered"] = False + state["_entered"] = 0 return state @@ -122,9 +125,21 @@ class IntRLock: """ def __init__(self): - self._buffer = bytearray(1) - # prevents buffer from being moved around by Python allocators - self._anchor = memoryview(self._buffer) + + # RemoteArray is a somewhat high-level data structure, + # which includes another byte for a lock - just + # to take account of the buffer life-cycle + # across interpreters. + + # unfortunatelly, I got no simpler mechanism than that + # to resolve the problem of the Lock object, along + # with the buffer being deleted in its owner interpreter + # while alive in a scondary one. + # (Remotearrays will go to a parking area, waiting until they + # are dereferenced remotely before freeing the memory) + + self._buffer = RemoteArray(size=1) + self._buffer._enter_parent() lock_str = _LockBuffer._from_data(self._buffer) self._lock = _CrossInterpreterStructLock(lock_str) @@ -156,10 +171,17 @@ def __exit__(self, *args): #self._lock.__exit__() def locked(self): - return bool(self._lock._entered) + if self._lock._entered: + return True + try: + self._lock.acquire(0) + except ResourceBusyError: + return True + self._lock.release() + return False def __getstate__(self): - return {"_lock": self._lock} + return {"_lock": self._lock, "_buffer": self._buffer} diff --git a/tests/test_lock.py b/tests/test_lock.py index 421fc4a..157b114 100644 --- a/tests/test_lock.py +++ b/tests/test_lock.py @@ -1,4 +1,5 @@ import pickle +from functools import partial from textwrap import dedent as D @@ -10,6 +11,21 @@ from extrainterpreters import Lock, RLock from extrainterpreters.lock import IntRLock + +@pytest.fixture +def interpreter(lowlevel): + interp = ei.Interpreter().start() + interp.run_string( + D( + f""" + import extrainterpreters as ei; ei.DEBUG=True + """ + ), + raise_=True + ) + yield interp + interp.close() + @pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock]) def test_locks_are_acquireable(LockCls): lock = LockCls() @@ -37,25 +53,31 @@ def test_lock_cant_be_reacquired_same_interpreter(): assert not lock.acquire(blocking=False) -@pytest.mark.skip("to be implemented") -def test_lock_cant_be_reacquired_other_interpreter(): - lock = Lock() - - lock.acquire() - with pytest.raises(TimeoutError): - lock.acquire(timeout=0) +def test_lock_cant_be_reacquired_other_interpreter(interpreter): + lock = Lock() + # some assertion lasagna - + # just checks basic toggling - no race conditions tested here: + run = partial(interpreter.run_string, raise_=True) + run(f"lock = pickle.loads({pickle.dumps(lock)})") + run (f"assert lock.acquire(blocking=False)") + assert not lock.acquire(blocking=False) + run (f"assert not lock.acquire(blocking=False)") + run (f"lock.release()") + assert lock.acquire(blocking=False) + run (f"assert not lock.acquire(blocking=False)") + lock.release() + run (f"assert lock.acquire(blocking=False)") + run (f"lock.release()") @pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock]) -def test_locks_can_be_passed_to_other_interpreter(LockCls, lowlevel): +def test_locks_can_be_passed_to_other_interpreter(LockCls, interpreter): lock = LockCls() lock_data = ei.utils._remote_memory(lock._lock._lock_address, 1) - interp = ei.Interpreter().start() - interp.run_string( + interpreter.run_string( D( f""" - import extrainterpreters as ei; ei.DEBUG=True lock = pickle.loads({pickle.dumps(lock)}) lock_data = ei.utils._remote_memory(lock._lock._lock_address, 1) assert lock_data[0] == 0 @@ -64,7 +86,7 @@ def test_locks_can_be_passed_to_other_interpreter(LockCls, lowlevel): raise_=True ) lock_data[0] = 2 - interp.run_string( + interpreter.run_string( D( """ assert lock_data[0] == 2 @@ -74,7 +96,6 @@ def test_locks_can_be_passed_to_other_interpreter(LockCls, lowlevel): raise_=True ) assert lock_data[0] == 5 - interp.close() #@pytest.mark.parametrize("LockCls", [Lock, RLock, IntRLock])