Repository navigation
multiprocessing.Event.set() can deadlock when called from a signal handler or trace function #126434
Copy link
Copy link
Open
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
multiprocessing.Event.set() will acquire a lock when setting the internal flag. multiprocessing.Event.is_set() will acquire the same lock when checking the flag. Thus if a signal handler calls .set() when .is_set() is running on the same process, there will be a deadlock.
multiprocessing.Event uses a regular non-reentrantlock lock. This should be changed to a reentrant lock. Please see the pull request.
Thanks for all the work on the Python programming language. I appreciate all your efforts highly.
Kind regards.
Example program below that (sometimes) deadlocks. On my machine I typically need to run it less than 10 times before a deadlock occurs. Also included in the code block is a sample stacktrace.
import faulthandler
import multiprocessing
import os
import signal
def run_buggy():
shutdown_event = multiprocessing.Event()
def sigterm_handler(_signo, _stack_frame):
try:
print(f'sigterm_handler running')
shutdown_event.set()
finally:
print(f'sigterm_handler done')
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigterm_handler)
faulthandler.register(signal.SIGUSR1)
print(f'Running process with PID {os.getpid()}')
print(f'Dump the stack by executing:')
print(f'kill -SIGUSR1 {os.getpid()}')
print(f'Try to kill this process with CTRL-C or kill {os.getpid()}')
while not shutdown_event.is_set():
pass
if __name__ == '__main__':
run_buggy()
# Sample stacktrace:
# Current thread 0x00000001ed74cf40 (most recent call first):
# File "/Users/ire/code/cpython/Lib/multiprocessing/synchronize.py", line 95 in __enter__
# File "/Users/ire/code/cpython/Lib/multiprocessing/synchronize.py", line 237 in __enter__
# File "/Users/ire/code/cpython/Lib/multiprocessing/synchronize.py", line 342 in set
# File "/Users/ire/code/cpython/./bug.py", line 13 in sigterm_handler
# File "/Users/ire/code/cpython/Lib/multiprocessing/synchronize.py", line 95 in __enter__
# File "/Users/ire/code/cpython/Lib/multiprocessing/synchronize.py", line 237 in __enter__
# File "/Users/ire/code/cpython/Lib/multiprocessing/synchronize.py", line 335 in is_set
# File "/Users/ire/code/cpython/./bug.py", line 25 in run_buggy
# File "/Users/ire/code/cpython/./bug.py", line 29 in <module>CPython versions tested on:
3.11, 3.12, CPython main branch
Operating systems tested on:
Linux, macOS
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-multiprocessingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
Zheaoli commentedon Nov 5, 2024
Bug confirmed on main branch
Zheaoli commentedon Nov 5, 2024
@Eclips4 We need tag for this issue, I think
topic-multiprocessingwould be OKBTW @ivarref are you already working on a PR?
ivarref commentedon Nov 5, 2024
Hi @Zheaoli
Thanks for the quick response. I've pushed a pull request now. What do you think?
Thanks and kind regards.
ivarref commentedon Nov 5, 2024
Update: I've added a blurb entry.
colesbury commentedon Nov 5, 2024
I commented on the PR, but I don't think
multiprocessing.Eventis safe to call from a Python signal handler, even with an RLock. I suspect that's true of a lot of Python's concurrency primitives.The general problem is that Python can execute signal handling code asynchronously at many places that don't expect to support reentrancy.
RLock's don't solve this problem because the things they protect are often not safe to call reentrancy. In other words, calling them reentrantly breaks invariants that a (non-reentrant) lock would otherwise protect.
pythongh-126434: Use multiprocessing.Value for multiprocessing.Event …
pythongh-126434: Add test
pythongh-126434: Remove print statements used/useful for debugging
pythongh-126434: Support systems with high load (no accidental timeouts)
pythongh-126434: Detect when .set() is called by a thread already .wa…
1 remaining item
pythongh-126434: Sleep longer.
pythongh-126434: Update blurb.
pythongh-126434: Document 64 bit example of how many times reentrant …
[-]multiprocessing.Event.set() can be deadlocked by .is_set when called by a sigterm handler[/-][+]multiprocessing.Event.set() can deadlock when called from a signal handler or trace function[/+]gpshead commentedon Nov 14, 2024
If people want workarounds for existing code: #85772 (comment) is the a good starting point. As much as I do not like any recommendation of spawning a thread to do the
Event.set()from the Python interrupting context, it works.Something I believe could be done to
Eventinstead is:with self._code:today are being called from is one which Python signal handlers will be invoked by. (this means "we're the main thread" - I'm being overly abstract)I'd rather we weren't in the business of spawning threads from multiprocessing at all, but I believe this will work and will work transparently. It is a new feature, we'd not backport it as a bug fix. But it also lines up nicely with 3.14's existing change of the default multiprocessing start method away from threading-unsafe "fork".
We could even adopt this conditionally based on the multiprocessing
ctx.get_start_method()in order to avoid the fork multithreaded process issues. Meaning the deadlock would still be possible for anyone using the "fork" start method. Not a huge deal given that "fork" and threading can already lead to other deadlocks? That'd just need documenting.(we can ignore the
sys.settracefunction use case I put in the title, that is an intentional footgun for anyone using it - I put it in the title for completeness as the issue is ultimately caused by anything diverting to execute other Python code at arbitrary points within one thread)colesbury commentedon Nov 14, 2024
I think the idea of calling
Event.set()from a different thread makes sense, but making that decision insideEventseems like playing whack-a-mole with asynchronous deadlock issues. This is an instance of a general problem: it's the same issue that comes up with GC finalizers running in the application thread and it's solved by running them in a dedicated thread 1.I think we should do something similar with Python signal handling: instead of handling signals on the main thread, Python should handle signals on a dedicated thread, perhaps as a configurable option to
signal.signal.Footnotes
The mainstream programming runtimes that support threads and use tracing GC (Java, Go, .NET) do this, with the exception of CPython. ↩
gpshead commentedon Nov 14, 2024
Those thoughts are in part why I was being abstract about where things are handled. Conversations about the signal handler being done in another thread have been slowly recurring over the years. Good to know that other languages actually do this.
A challenge if we adopt this approach is that people expect as an API that exceptions escaping a signal handlers are injected into the main thread. That seems solvable even if handler execution happens on another thread: Keep the existing pending signal check logic and turn that into a "pending exception from a signal handler to reraise" check.
In this world, which thread signals are handled in and which thread sees exceptions escaping a signal handler interrupt it could both be independent concepts, potentially configurable (I'd prefer to avoid letting people do unusual things with such process global settings for consistency of expectations).
colesbury commentedon Nov 14, 2024
My comment may not have been clear: I meant that other languages run finalizers in a dedicated thread, although
.NETalso appears to run signal handlers in a different thread. Go apparently does signal handling via channels, which allows the user to control the thread. As far as I can tell, the Java standard library does not provide a way for applications to handle UNIX signals.This would reintroduce the deadlocks and corruption issues that using a separate thread avoids. Python code isn't robust to exceptions being raised at arbitrary places. For example, it can corrupt the state of
threading.Conditionor anything else that relies ontry/finallyor context managers with__exit__. (If the exception is asynchronously raised at the start of afinallyblock, then the remainder of the finally block won't run.)ivarref commentedon Nov 15, 2024
@gpshead
I don't think that would quite work. Imagine the following:
Thus, you would need to fire and forget (not wait for the result). That would potentially cause other problems.
How about adding the following argument to
signal.signalin 3.14:signal.signal(signalnum, handler, dedicated_thread=True)If
signal.signalis never called, what should the default behaviour be?Java has a method called addShutdownHook. The input parameter for that function is a not-started thread. The input parameter (thread) will be started and executed when the JVM is about to exit, for example upon encountering SIGINT, etc. Thus SIGINT will not be processed inside/on top of already running code.
Java's Thread.sleep will throw an InterruptedException if the thread is, well, interrupted. This will happen on SIGINT as far as I know.
Thus I suppose that Python's
time.sleepand similar could also raise an exception if SIGINT is received and there is no signal handler. I suppose the default signal handler could set some sort of interrupt flag and/or have functions like time.sleep raise exceptions. I don't see any fields for it in e.g.Threadthough (unlike in Java).A dedicated signal thread that can only process one signal at a time seems like the right approach to me. But what is the possible breakage here, as @gpshead writes "expect as an API that exceptions escaping a signal handlers are injected into the main thread", ?
How about this for the default behaviour in 3.14: Signal handling run on a dedicated thread. SIGINT, etc. will cause thread(s) to shutdown if no signal handler is installed.
Is the current default behaviour, with no signal handlers installed, of Python to upon receiving SIGINT, to interrupt the main thread and let the other non-daemon threads finish as they please?
Perhaps there is not that much to worry about: if the existing code does not do signal handling OR raises exceptions from the signal handler, which I suppose this approximately amounts to the same thing, is it fine to shutdown the program (or main thread only?) in such a case. The only difference is that the shutdown initiation comes from a different thread.
I don't know much about Python's shutdown, signals and threads, so I'm uncertain how helpful my comments are, and how much work it would be to implement the suggestions described here. Thanks and kind regards even so :-)
pythongh-126434: Process signals on dedicated thread
ivarref commentedon Nov 28, 2024
Hi again @colesbury and @gpshead
I've done a new take on this. It effectively runs signal handlers on a dedicated thread*.
The new signal.py code is highlighted here.
It is backwards compatible if the argument
use_dedicated_thread=Falseis passed to tosignal.signal. The default istrue.It will bubble an uncaught exception in a signal handler to the main thread. (Do you think this makes sense?)
The change is only in python code. The original C code from
signalmodule.cis left untouched.* It uses
queue.SimpleQueueto achieve this, asqueue.SimpleQueue.putis reentrant and can thus be used from actual signal handlers.Here is an example program that uses the new signal handler:
The committed code could certainly be cleaned up. No tests currently exists. I'd be happy to add that. But first I'd like to ask:
is this a viable path forwards? Using only Python to implement this and leaving the old C code as is**? Are there other considerations that must be done?
** I don't think I'm competent wrt. C and signals to write new C code.
Thanks and kind regards.
pythongh-126434: Continue process signals on dedicated thread, don't …
pythongh-126434: Start fix tests when using use_dedicated_thread=True…
pythongh-126434: What should be done if sys.exit() is called from a s…
pythongh-126434: Propagate sys.exit from signal handler to main thread