Issue2328

classification
Title: RuntimeError: threading.Lock cannot be released by threads other than the one that acquired it
Type: Severity: normal
Components: Library Versions: Jython 2.7
Milestone: Jython 2.7.0
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: zyasoft Nosy List: jmadden, zyasoft
Priority: urgent Keywords: patch

Created on 2015-04-14.12:37:10 by jmadden, last changed 2015-04-27.01:53:39 by zyasoft.

Files
File name Uploaded Description Edit Remove
use-semaphore-for-lock.patch zyasoft, 2015-04-14.18:31:47 Use java.util.concurrent.Semaphore to implement threading.Lock
use-mutex-example-for-lock.patch jmadden, 2015-04-14.20:48:59
use-mutex-example-for-lock2.patch jmadden, 2015-04-14.22:42:17
Messages
msg9834 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.12:37:10
The documentation for `threading.Lock` says (emphasis added):

    A factory function that returns a new primitive lock object. Once a thread has acquired it, subsequent attempts to acquire it block, until it is released; *any thread may release it*.

This is in contrast with `threading.RLock` which specifically says that a "reentrant lock must be released by the thread that acquired it."

Jython uses the same implementation for both types of locks (Lock.java), and it obeys the semantics of `RLock` by requiring that the releasing thread be the acquiring thread, raising an exception if that's not the case. This can cause problems for applications that make advanced use of locks. 

Consider this example in Jython:

  $ ../jython/dist/bin/jython
  Jython 2.7rc2+ (default:5064a5c5b1a3, Apr 14 2015, 06:28:43)
  [Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)] on java1.8.0_40
  Type "help", "copyright", "credits" or "license" for more information.
  >>> import threading
  >>> threading.Lock is threading.RLock
  True
  >>> lock = threading.Lock()
  >>> lock.acquire()
  True
  >>> t = threading.Thread(target=lock.release)
  >>> t.start(); t.join()
  >>> Exception in thread Thread-1:Traceback (most recent call last):
    File "//jython/dist/Lib/threading.py", line 222, in _Thread__bootstrap
      self.run()
    File "//jython/dist/Lib/threading.py", line 213, in run
      self._target(*self._args, **self._kwargs)
  RuntimeError: cannot release un-acquired lock
  >>> lock.locked()
  True
 
Compare to the CPython and PyPy behaviour:

  $ /opt/local/bin/python2.7
  Python 2.7.9 (default, Dec 13 2014, 15:13:49)
  [GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.56)] on darwin
  Type "help", "copyright", "credits" or "license" for more information.
  >>> import threading
  >>> threading.Lock is threading.RLock
  False
  >>> lock = threading.Lock()
  >>> lock.acquire()
  True
  >>> t = threading.Thread(target=lock.release)
  >>> t.start(); t.join()
  >>> lock.locked()
  False

One real-world example of this is ZODB, at least its test suite. Exceptions like the following are fairly common (fortunately releasing the lock is the last thing the thread was going to do, and then the lock gets destroyed anyway so it's not an immediate issue in this case---the failure has no consequences for the test):

  Exception in thread commit:Traceback (most recent call last):
  File "//jython/dist/Lib/threading.py", line 222, in _Thread__bootstrap
    self.run()
  File "//jython/dist/Lib/threading.py", line 213, in run
    self._target(*self._args, **self._kwargs)
  File "//ZODB/src/ZODB/tests/BasicStorage.py", line 343, in commit
    self._storage.tpc_finish(t, callback)
  File "//ZODB/src/ZODB/utils.py", line 293, in __call__
    return func(*args, **kw)
  File "//ZODB/src/ZODB/DemoStorage.py", line 349, in tpc_finish
    self.changes.tpc_finish(transaction, func)
  File "//ZODB/src/ZODB/utils.py", line 293, in __call__
    return func(*args, **kw)
  File "//ZODB/src/ZODB/MappingStorage.py", line 318, in tpc_finish
    self._commit_lock.release()
  RuntimeError: cannot release un-acquired lock
msg9837 (view) Author: Jim Baker (zyasoft) Date: 2015-04-14.15:25:52
Makes sense. We can change the underlying implementation to use a Semaphore with count of 1, and that should provide the desired semantics. Should be an easy fix, assuming it doesn't break anything else out there.
msg9840 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.15:32:19
If RLock and Lock are still sharing the same implementation though, wouldn't the Semaphore(1) approach then turn around and break RLock (which has to be acquirable and releasable an arbitrary number of times, albeit by the same thread)? 

I haven't studied the implementation that much, but I was imagining something like making Lock a base class (AbstractLock) with a flag set by its subclasses Lock and RLock controlling whether 'isHeldByCurrentThread' is checked on release or not. But that was just my first thought and I don't know that much about how Java classes get exposed to Python. Or maybe you also meant a separate implementation of Lock with Semaphore and using the current implementation as RLock?
msg9842 (view) Author: Jim Baker (zyasoft) Date: 2015-04-14.15:45:04
Two completely different behaviors, so we will want to separate in this fix:

threading.RLock will (continue) to use java.util.concurrent.locks.ReentrantLock

threading.Lock will now use java.util.concurrent.Semaphore
msg9843 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.15:50:03
My apologies for the misunderstanding. I agree that a Semaphore(1)-based approach for Lock should be compatible with the CPython semantics.
msg9844 (view) Author: Jim Baker (zyasoft) Date: 2015-04-14.18:31:47
So I looked into this, and got pretty far, with all lock tests passing, including ones previously skipped. Patch attached (use-semaphore-for-lock.patch).

However the problem is that threading.Condition is built so that it can take Lock.newCondition, which means it needs a class that implements the Lock interface.

We can readily implement this with lower-level synchronization primitives provided by java.util.concurrent, most likely LockSupport, but it's unfortunately too late for RC3.

Target 2.7.1
msg9846 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.19:19:08
The j.u.c.locks.ReentrantLock class is built upon the standard AbstractQueuedSynchronizer class. The current JavaDoc for that class (http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/AbstractQueuedSynchronizer.html) defines a complete /non-reentrant/ implementation of the j.u.c.locks.Lock interface, including Condition support. It's only a dozen or so lines and can be unlocked by any thread.

Perhaps that could form the basis for the threading.Lock instead of a Semaphore?
msg9849 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.20:48:59
In the attached use-mutex-example-for-lock.patch, I tried the approach of using the Mutex example for Lock and sticking with ReentrantLock for RLock. There had to be a bit of abstraction introduced for Condition to work correctly. 

It's not the prettiest Java I've ever written, but at least as a proof-of-concept it seems to work. All the tests in test_threading and test_thread pass (along with as much of regrtest-unix as I can get through---my machine hangs out of files around test_httpservers and I haven't adjusted my ulimits yet). The only exception is the newly-enabled `test_notify` in test_threading.ConditionTests. Enabling that test was part of the earlier patch, but the code in Condition to implement `notify(n)` didn't make it in and I'm not 100% sure how to deal with overloaded methods yet so I didn't try to add it. 

(Also, the exposed name of RLock is coming out wrong right now and I don't yet know enough to fix it:

$ dist/bin/jython
imporJython 2.7rc2+ (default:5064a5c5b1a3+, Apr 14 2015, 15:38:48)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)] on java1.8.0_40
Type "help", "copyright", "credits" or "license" for more information.
>>> import threading
>>> threading.RLock
<type 'org.python.modules._threading.RLock'>
>>> threading.Lock
<type '_threading.Lock'>
)
msg9850 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.22:42:17
The second version of the patch cleans things up a little bit to remove the abstract class that was exposed to Python in the `mro()`. It also fixes the `test_notify` test. 

I tried adding RLock to CoreExposed.includes to fix the name, but that made things worse. Clearly I still have no idea what I'm doing when it comes to that :)
msg9851 (view) Author: Jim Baker (zyasoft) Date: 2015-04-14.22:44:37
Jason, thanks for trying out the AbstractQueuedSynchronizer example code, and figuring out how to fit in with the approach I sketched out. This looks great, and given that the example is well tested in terms of AQS we should be able to get this into RC3 after all.
msg9852 (view) Author: Jim Baker (zyasoft) Date: 2015-04-14.22:46:49
Also, the newest patch looks even better.
msg9853 (view) Author: Jim Baker (zyasoft) Date: 2015-04-14.22:50:05
Don't worry about the Jython adaptation, I can readily make that happen. It's a bit of a mix of example code, my attempt, and your contributions, but if you haven't done so already, please sign https://www.python.org/psf/contrib/contrib-form/ so we can get this committed. Thanks again!
msg9854 (view) Author: Jason Madden (jmadden) Date: 2015-04-14.23:01:21
Thank you! The PSF agreement was one I don't think I'd signed yet---done.
msg9887 (view) Author: Jim Baker (zyasoft) Date: 2015-04-19.15:52:51
Fixed as of https://hg.python.org/jython/rev/0e103b371f5a
msg9889 (view) Author: Jim Baker (zyasoft) Date: 2015-04-19.18:36:15
With some more changes in the subsequent fix in https://hg.python.org/jython/rev/dd4327161042 - specifically this fixed the issue raised by msg9849 around repr - and made it more like CPython as well in terms of reflecting the state of the Condition, Lock, and RLock
History
Date User Action Args
2015-04-27 01:53:39zyasoftsetstatus: pending -> closed
2015-04-19 18:36:15zyasoftsetmessages: + msg9889
2015-04-19 15:52:51zyasoftsetstatus: open -> pending
assignee: zyasoft
resolution: accepted -> fixed
messages: + msg9887
2015-04-14 23:33:01zyasoftsetpriority: high -> urgent
milestone: Jython 2.7.0
2015-04-14 23:01:21jmaddensetmessages: + msg9854
2015-04-14 22:50:05zyasoftsetmessages: + msg9853
2015-04-14 22:46:49zyasoftsetmessages: + msg9852
2015-04-14 22:44:37zyasoftsetmessages: + msg9851
2015-04-14 22:42:17jmaddensetfiles: + use-mutex-example-for-lock2.patch
messages: + msg9850
2015-04-14 20:49:00jmaddensetfiles: + use-mutex-example-for-lock.patch
messages: + msg9849
2015-04-14 19:19:08jmaddensetmessages: + msg9846
2015-04-14 18:31:48zyasoftsetpriority: high
files: + use-semaphore-for-lock.patch
resolution: accepted
messages: + msg9844
keywords: + patch
2015-04-14 15:50:03jmaddensetmessages: + msg9843
2015-04-14 15:45:04zyasoftsetmessages: + msg9842
2015-04-14 15:32:20jmaddensetmessages: + msg9840
2015-04-14 15:25:52zyasoftsetnosy: + zyasoft
messages: + msg9837
2015-04-14 12:37:10jmaddencreate