Description
Bug report
I found a bug that seems to be code corruption.
While working on an example project with ~70 threads, I occasionally (once every hour or so) get the following exception from various locks:
Exception in thread Sequence 2:
Traceback (most recent call last):
File "/Volumes/RAMDisk/installed-nogil-main/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/threading.py", line 1039, in _bootstrap_inner
self.run()
~~~~~~~~^^
...
File "/Volumes/RAMDisk/installed-nogil-main/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/threading.py", line 656, in wait
with self._cond:
^^^^^^^^^^
File "/Volumes/RAMDisk/installed-nogil-main/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/threading.py", line 304, in __enter__
return self._lock.__enter__()
~~~~~~~~~~~~~~~~~~~~^^
TypeError: descriptor '__exit__' for '_thread.RLock' objects doesn't apply to a '_thread.lock' object
or
Exception in thread Clock:
Traceback (most recent call last):
File "/Volumes/RAMDisk/installed-nogil-main/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/threading.py", line 1039, in _bootstrap_inner
self.run()
~~~~~~~~^^
File "europython.py", line 113, in run
for message in input:
File "/Volumes/RAMDisk/nogil-ep-temp/lib/python3.14/site-packages/mido/ports.py", line 243, in __iter__
yield self.receive()
~~~~~~~~~~~~^^
File "/Volumes/RAMDisk/nogil-ep-temp/lib/python3.14/site-packages/mido/ports.py", line 215, in receive
with self._lock:
^^^^^^^^^^
TypeError: descriptor '__exit__' for '_thread.lock' objects doesn't apply to a '_thread.RLock' object
This looks like it's a bug related to locks but it isn't. It's not even related to descriptors, only descriptors nicely refuse running invalid code.
This issue is also externally reported in PyWavelets/pywt#758 with the same Lock descriptor error message I've seen, and I can reproduce the failure locally, albeit with a different exception:
TypeError: descriptor 'sort' for 'numpy.ndarray' objects doesn't apply to a 'ThreadPoolExecutor' object
To reproduce this with cpython main
, do the following:
- make a venv with a free-threaded build of Python
- install Cython from main with
pip install -e .
- install Numpy from main with
pip install . --no-build-isolation
(important: no-e
in this case) - install pywt from main with
pip install -e . --no-build-isolation
(important: you DO need-e
in this case) - run pytest in a loop (or with autoclave) like this:
PYTHON_GIL=0 pytest pywt/tests/test_concurrent.py
You will need to run this for a longer while to get to a failure.
By doing this, I managed to find this particular failure case:
self = <concurrent.futures.thread.ThreadPoolExecutor object at 0x225be124750>, fn = functools.partial(<function dwtn at 0x225be6d3b40>, wavelet='haar')
args = (array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0., 0., ...., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]),)
kwargs = {}, f = <Future at 0x225bea44310 state=finished returned dict>, w = <concurrent.futures.thread._WorkItem object at 0x225bc875bf0>
def submit(self, fn, /, *args, **kwargs):
with self._shutdown_lock, _global_shutdown_lock:
if self._broken:
raise BrokenThreadPool(self._broken)
if self._shutdown:
raise RuntimeError('cannot schedule new futures after shutdown')
if _shutdown:
raise RuntimeError('cannot schedule new futures after '
'interpreter shutdown')
f = _base.Future()
w = _WorkItem(f, fn, args, kwargs)
self._work_queue.put(w)
> self._adjust_thread_count()
E TypeError: Future.set_result() missing 1 required positional argument: 'result'
args = (array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0., 0., 0., ...., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]]),)
f = <Future at 0x225bea44310 state=finished returned dict>
fn = functools.partial(<function dwtn at 0x225be6d3b40>, wavelet='haar')
kwargs = {}
self = <concurrent.futures.thread.ThreadPoolExecutor object at 0x225be124750>
w = <concurrent.futures.thread._WorkItem object at 0x225bc875bf0>
../installed-nogil-main/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/concurrent/futures/thread.py:179: TypeError
======================================================================================================== short test summary info ========================================================================================================
FAILED pywt/tests/test_concurrent.py::test_concurrent_dwt - TypeError: Future.set_result() missing 1 required positional argument: 'result'
====================================================================================================== 1 failed, 3 passed in 0.44s ======================================================================================================
-- 863 runs, 862 passes, 1 failure, 734486 msec
Observe how Python wants to call self._adjust_thread_count()
(with no arguments) but ends up calling f.set_result()
, which causes an exception due to no arguments being passed.
Tested on macOS Sonoma on M1 Max with Python 3.14.0a0 experimental free-threading build (heads/main:7a807c3efaa, Jul 2 2024, 11:58:38).
AFAICT the problem only occurs with the GIL actually disabled.