Reputation: 31
I'm trying to timeout a function if it runs for more than 3 seconds (for example). I'm using signals and alarms but the alarm never fires. I'd like a timeout mechanism that works for any function. As an example of the problem I'm facing:
import signal
def foobar():
x = 42
while x >= 20:
if x >= 40:
x = 23
return x
def handle_alarm(*args):
print("Alarm raised")
raise TimeoutException("timeout reached")
signal.signal(signal.SIGALRM, handle_alarm)
signal.alarm(3)
try:
print(foobar())
except:
print("Exception Caught")
When run, my program just runs forever and my handler never runs. Any idea why this is the case?
As an aside, if I delete the if statement from foobar, then the alarm does trigger.
Upvotes: 2
Views: 1104
Reputation: 249133
On my system, Mac OS X with MacPorts, I tested your code with many version of Python. The only version that exhibits the "bug" you found is 2.7. The timeout works in 2.4, 2.5, 2.6, 3.3, and 3.4.
Now, why is this happening, and what can be done about it?
I think it happens because your foobar()
is a tight loop which never "yields" control back to Python's main loop. It just runs as fast as it can, doing no useful work, yet preventing Python from processing the signal.
It will help to understand how signals in *nix are usually handled. Since few library functions are "async signal safe," not much can be done within a C signal handler directly. Python needs to invoke your signal handler which is written in Python, but it can't do that directly in the signal handler that it registers using C. So a typical thing that programs do in their signal handlers is to set some flag to indicate that a signal has been received, and then return. In the main loop, then, that flag is checked (either directly or by using a "pipe" which can be written to in the signal handler and poll()
ed or select()
ed on).
So I would suppose that the Python main loop is happily executing your foobar()
function, and a signal comes in, it sets some internal state to know it needs to handle that signal, and then it waits for for foobar()
to end, or failing that, at least for foobar()
to invoke some interruptible function, such as sleep()
or print()
.
And indeed, if you add either a sleep (for any amount of time), or a print
statement to foobar()
's loop, you will get the timeout you desire in Python 2.7 (as well as the other versions).
It is in general a good idea to put a short sleep in busy loops anyway, to "relax" them, thereby helping scheduling of other work which may need doing. You don't have to sleep on every iteration either--just a tiny sleep every 1000 times through the loop would work fine in this case.
Upvotes: 3