-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Daemonic threads not killed in some circumstances in python 3.13 #123940
Comments
Joining daemon threads at finalizer of diff --git a/src/zlib_ng/gzip_ng_threaded.py b/src/zlib_ng/gzip_ng_threaded.py
index 7fa7249..65bd95c 100644
--- a/src/zlib_ng/gzip_ng_threaded.py
+++ b/src/zlib_ng/gzip_ng_threaded.py
@@ -156,7 +156,6 @@ class _ThreadedGzipReader(io.RawIOBase):
if self._closed:
return
self.running = False
- self.worker.join()
self.fileobj.close()
if self.closefd:
self.raw.close()
@@ -329,7 +328,7 @@ class _ThreadedGzipWriter(io.RawIOBase):
if self._closed:
return
self.flush()
- self.stop()
+ self.running = False
if self.exception:
self.raw.close()
self._closed = True Is it possible to workaround it by starting the worker in first call to |
@y5c4l3 Thanks! I will investigate and report back. |
Reproducible: # mwe.py
import io
import threading
from spawner import ModuleSpawnerIO
class SpawnerIO(io.RawIOBase):
def __init__(self):
self.task = threading.Thread(target=self._process, daemon=True)
self.task.start()
def _process(self):
while True:
pass
def readable(self):
return True
def close(self):
print('finalizing...')
self.task.join()
# exit as expected, not calling finalizer
# r = io.BufferedReader(SpawnerIO())
# finalized and stuck
r = io.BufferedReader(ModuleSpawnerIO()) # spawner.py
import io
import threading
class ModuleSpawnerIO(io.RawIOBase):
def __init__(self):
self.task = threading.Thread(target=self._process, daemon=True)
self.task.start()
def _process(self):
while True:
pass
def readable(self):
return True
def close(self):
print('finalizing...')
self.task.join() Wrapping Lines 294 to 302 in 1de4613
Simpler Reproducible with
|
Thanks for the minimal repo! I can take a look on Monday. |
@hauntsaninja - What were you using as the test case for bisection? The simpler reproducer using |
I was using the mwe.py + spawner.py repro (which exits in 3.12.5). I think y5c4l3 edited in the |
During finalization, daemon threads are force to exit immediately (without returning through the call-stack normally) upon acquiring the GIL. Finalizers that run after this must be able to join the forcefully terminated threads. The current implemenation notified of thread completetion before returning from `thread_run`. This code will never execute if the thread is forced to exit during finalization. Any code that attempts to join such a thread will block indefinitely. To fix this, use the old approach of notifying of thread completion when the thread state is cleared. This happens both when `thread_run` exits normally and when thread states are destroyed as part of finalization (which happens immediately after forcing daemon threads to exit, before any python code can run).
During finalization, daemon threads are force to exit immediately (without returning through the call-stack normally) upon acquiring the GIL. Finalizers that run after this must be able to join the forcefully terminated threads. The current implementation notified of thread completion before returning from `thread_run`. This code will never execute if the thread is forced to exit during finalization. Any code that attempts to join such a thread will block indefinitely. To fix this, use the old approach of notifying of thread completion when the thread state is cleared. This happens both when `thread_run` exits normally and when thread states are destroyed as part of finalization (which happens immediately after forcing daemon threads to exit, before any python code can run).
During finalization, daemon threads are force to exit immediately (without returning through the call-stack normally) upon acquiring the GIL. Finalizers that run after this must be able to join the forcefully terminated threads. The current implementation notified of thread completion before returning from `thread_run`. This code will never execute if the thread is forced to exit during finalization. Any code that attempts to join such a thread will block indefinitely. To fix this, use the old approach of notifying of thread completion when the thread state is cleared. This happens both when `thread_run` exits normally and when thread states are destroyed as part of finalization (which happens immediately after forcing daemon threads to exit, before any python code can run).
Matt is working on a fix, but I don't think the approach used in https://github.com/pycompression/python-zlib-ng is a good idea: In particular:
On the second point: both the join and shutdown behavior has changed in recent releases and is likely to continue to change even with Matt's fix. For example, Finally, slight variants of the reproducer will hard crash in 3.11 and 3.12 (and possibly earlier versions), which makes me think it's not a robust approach. |
There is no custom destructors in python-zlib-ng that join threads. Where did you see this code? In fact, for the classes that cause the issue, there are no custom destructors.
I really appreciate your explanation. What patterns do you recommend to avoid this sort of behaviour? I have a thread that does the IO. Are there any well documented patterns to prevent this from blocking? There should be at least some documentation on this in the release notes if it is going to break currently used code. Python-isal uses the same code and is marked as an essential package for the python ecosystem. I'd like it to be able to work with python 3.13 when it comes out. |
The
Don't call class _ThreadedGzipReader(io.RawIOBase):
def stop(self):
if self.running:
self.running = False
if not self._finalizing:
for worker in self.compression_workers:
worker.join()
self.output_worker.join()
def __del__(self):
self._finalizing = True
super().__del__() |
@colesbury Thank you so much for this additional information. Now I understand the problem and can fix it appropriately. |
People are leaning towards not treating this as blocking for 3.13.0 (x-ref colesbury's message on internal Discord) colesbury also mentioned #116514 as a reference for hard crashes on 3.11/3.12 (I wasn't able to reproduce #124150 (comment) ) |
Bug report
Bug description:
I got a bug report on python-zlib-ng where using the threaded opening the program would hang if an exception occurred and a context manager was not used.
I fixed it by changing the threads to be daemonic: pycompression/python-zlib-ng#54.
This fixes the issue on python 3.9, 3.10, 3.11 and 3.12, but not 3.13-rc2. Also the latest main branch is affected. Unfortunately I could not create a minimal reproducer, other than running the test directly. Daemonic threads seem not to work in this particular case, where there are some BufferedIO streams and queues involved.
Since there are many code changes between 3.12 in 3.13 the underlying cause is surely there, but it is hard to pinpoint.
CPython versions tested on:
3.13, CPython main branch
Operating systems tested on:
Linux
Linked PRs
The text was updated successfully, but these errors were encountered: