31

In Python, for a toy example:

for x in range(0, 3):
    # Call function A(x)

I want to continue the for loop if function A takes more than five seconds by skipping it so I won't get stuck or waste time.

By doing some search, I realized a subprocess or thread may help, but I have no idea how to implement it here.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user2372074
  • 781
  • 3
  • 7
  • 18
  • Are you on Windows or Unix? – TheSoundDefense Jul 30 '14 at 00:44
  • 1
    @TheSoundDefense mac os – user2372074 Jul 30 '14 at 00:45
  • 5
    I think having a check in the function you call to avoid getting stuck would be a smarter move, unless you are doing some heavy calculations or something has gone very wrong, you should not be stuck for 5 seconds, if you have a lot of data then maybe multiprocessing would be the way to go. – Padraic Cunningham Jul 30 '14 at 00:46
  • @Padraic Cunningham Thanks for replay. It won't work in my case since the function may experience network or CPU idling. – user2372074 Jul 30 '14 at 00:47
  • @user2372074: Do you only need this to work on Mac OS X (or at least only on Mac OS X and other reasonably modern Unix and Unix-like systems)? If so, the answer is definitely simpler. (Signals are easy in Python; Windows APCs or similar mechanisms are not…) – abarnert Jul 30 '14 at 01:04

5 Answers5

50

I think creating a new process may be overkill. If you're on Mac or a Unix-based system, you should be able to use signal.SIGALRM to forcibly time out functions that take too long. This will work on functions that are idling for network or other issues that you absolutely can't handle by modifying your function. I have an example of using it in this answer:

Option for SSH to timeout after a short time? ClientAlive & ConnectTimeout don't seem to do what I need them to do

Editing my answer in here, though I'm not sure I'm supposed to do that:

import signal

class TimeoutException(Exception):   # Custom exception class
    pass

def timeout_handler(signum, frame):   # Custom signal handler
    raise TimeoutException

# Change the behavior of SIGALRM
signal.signal(signal.SIGALRM, timeout_handler)

for i in range(3):
    # Start the timer. Once 5 seconds are over, a SIGALRM signal is sent.
    signal.alarm(5)    
    # This try/except loop ensures that 
    #   you'll catch TimeoutException when it's sent.
    try:
        A(i) # Whatever your function that might hang
    except TimeoutException:
        continue # continue the for loop if function A takes more than 5 second
    else:
        # Reset the alarm
        signal.alarm(0)

This basically sets a timer for 5 seconds, then tries to execute your code. If it fails to complete before time runs out, a SIGALRM is sent, which we catch and turn into a TimeoutException. That forces you to the except block, where your program can continue.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
TheSoundDefense
  • 6,753
  • 1
  • 30
  • 42
  • I'm pretty sure you're supposed to do that… unless it's effectively the same question and deserves exactly the same answer, in which case it's probably better to close as a dup than to copy your code over. – abarnert Jul 30 '14 at 00:57
  • @TheSoundDefense I got an error saying: TypeError: exceptions must be old-style classes or derived from BaseException, not funtion – user2372074 Jul 30 '14 at 13:45
  • @user2372074 what version of Python are you using? And what line was the error on? – TheSoundDefense Jul 30 '14 at 13:59
  • @TheSoundDefense i'm using python 2.7.5. I fixed the error by changing "def TimeoutException(Exception):" to "class TimeoutException(Exception):". It works but I don't know why. – user2372074 Jul 30 '14 at 16:37
  • 1
    @TheSoundDefense: No, that's not right, the OP found the right solution. His change (using `class`) means he's defining a subclass of `Exception`, which is exactly what he wants. Your original code (using `def`) just defines a function whose parameter happens to be named `Exception`, and then tries to use that function as an exception class. – abarnert Jul 30 '14 at 17:58
  • @abarnert oh geez, I did not even realize that I typed `def` instead of `class`. That was not my intention. – TheSoundDefense Jul 30 '14 at 18:00
  • 2
    @TheSoundDefense: Yeah, I figured. Who hasn't made that kind of silly mistake? And who's ever noticed it in his own code without at least 6 hours of bashing his head against the monitor? – abarnert Jul 30 '14 at 18:31
  • 1
    I've adapted the code for the question. Feel free to rollback. – jfs Jul 31 '14 at 17:15
  • How do I pass a fractional value to `signal.alarm()` ? I want to do `signal.alarm(0.25)` to set a time-out limit of 0.25 seconds. It seems I can only pass integers to the function. How do I do this? – mjsxbo Dec 21 '17 at 07:00
  • @TheSoundDefense I found if a deadlock occurs in `A(i)`, the alarm signal will not works. How to fix this problem? – formath Oct 30 '18 at 03:59
  • 3
    Note: signal does NOT with multithreading. It only works on the main thread. – Gwi7d31 Apr 06 '21 at 20:33
16

Maybe someone find this decorator useful, based on TheSoundDefense answer:

import time
import signal

class TimeoutException(Exception):   # Custom exception class
    pass


def break_after(seconds=2):
    def timeout_handler(signum, frame):   # Custom signal handler
        raise TimeoutException
    def function(function):
        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(seconds)
            try:
                res = function(*args, **kwargs)
                signal.alarm(0)      # Clear alarm
                return res
            except TimeoutException:
                print u'Oops, timeout: %s sec reached.' % seconds, function.__name__, args, kwargs
            return
        return wrapper
    return function

Test:

@break_after(3)
def test(a, b, c):
    return time.sleep(10)

>>> test(1,2,3)
Oops, timeout: 3 sec reached. test (1, 2, 3) {}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Igor Kremin
  • 355
  • 4
  • 8
12

If you can break your work up and check every so often, that's almost always the best solution. But sometimes that's not possible—e.g., maybe you're reading a file off an slow file share that every once in a while just hangs for 30 seconds. To deal with that internally, you'd have to restructure your whole program around an async I/O loop.

If you don't need to be cross-platform, you can use signals on *nix (including Mac and Linux), APCs on Windows, etc. But if you need to be cross-platform, that doesn't work.

So, if you really need to do it concurrently, you can, and sometimes you have to. In that case, you probably want to use a process for this, not a thread. You can't really kill a thread safely, but you can kill a process, and it can be as safe as you want it to be. Also, if the thread is taking 5+ seconds because it's CPU-bound, you don't want to fight with it over the GIL.

There are two basic options here.


First, you can put the code in another script and run it with subprocess:

subprocess.check_call([sys.executable, 'other_script.py', arg, other_arg],
                      timeout=5)

Since this is going through normal child-process channels, the only communication you can use is some argv strings, a success/failure return value (actually a small integer, but that's not much better), and optionally a hunk of text going in and a chunk of text coming out.


Alternatively, you can use multiprocessing to spawn a thread-like child process:

p = multiprocessing.Process(func, args)
p.start()
p.join(5)
if p.is_alive():
    p.terminate()

As you can see, this is a little more complicated, but it's better in a few ways:

  • You can pass arbitrary Python objects (at least anything that can be pickled) rather than just strings.
  • Instead of having to put the target code in a completely independent script, you can leave it as a function in the same script.
  • It's more flexible—e.g., if you later need to, say, pass progress updates, it's very easy to add a queue in either or both directions.

The big problem with any kind of parallelism is sharing mutable data—e.g., having a background task update a global dictionary as part of its work (which your comments say you're trying to do). With threads, you can sort of get away with it, but race conditions can lead to corrupted data, so you have to be very careful with locking. With child processes, you can't get away with it at all. (Yes, you can use shared memory, as Sharing state between processes explains, but this is limited to simple types like numbers, fixed arrays, and types you know how to define as C structures, and it just gets you back to the same problems as threads.)


Ideally, you arrange things so you don't need to share any data while the process is running—you pass in a dict as a parameter and get a dict back as a result. This is usually pretty easy to arrange when you have a previously-synchronous function that you want to put in the background.

But what if, say, a partial result is better than no result? In that case, the simplest solution is to pass the results over a queue. You can do this with an explicit queue, as explained in Exchanging objects between processes, but there's an easier way.

If you can break the monolithic process into separate tasks, one for each value (or group of values) you wanted to stick in the dictionary, you can schedule them on a Pool—or, even better, a concurrent.futures.Executor. (If you're on Python 2.x or 3.1, see the backport futures on PyPI.)

Let's say your slow function looked like this:

def spam():
    global d
    for meat in get_all_meats():
        count = get_meat_count(meat)
        d.setdefault(meat, 0) += count

Instead, you'd do this:

def spam_one(meat):
    count = get_meat_count(meat)
    return meat, count

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    results = executor.map(spam_one, get_canned_meats(), timeout=5)
    for (meat, count) in results:
        d.setdefault(meat, 0) += count

As many results as you get within 5 seconds get added to the dict; if that isn't all of them, the rest are abandoned, and a TimeoutError is raised (which you can handle however you want—log it, do some quick fallback code, whatever).

And if the tasks really are independent (as they are in my stupid little example, but of course they may not be in your real code, at least not without a major redesign), you can parallelize the work for free just by removing that max_workers=1. Then, if you run it on an 8-core machine, it'll kick off 8 workers and given them each 1/8th of the work to do, and things will get done faster. (Usually not 8x as fast, but often 3-6x as fast, which is still pretty nice.)

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • thanks for your generous help. I did the second way. But I have an issue here. The func I'm calling won't update a global variable. What I'm doing here is adding some data to the global dic which didn't happen. Could you give me some idea? – user2372074 Jul 30 '14 at 13:16
  • @user2372074: In `multiprocessing`, you can't directly share global variables. (Or, worse, it'll sort of work—e.g., raise an exception on OS X, usually but not quite always work on other *nix, and silently update a copy instead of the original on Windows.) Go through the [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) docs (just Introduction and Programming Guidelines; skip over the Reference), but the best solution is usually to use a queue to pass the objects between processes, and I'll edit an example of that into my answer. – abarnert Jul 30 '14 at 17:23
4

This seems like a better idea (sorry, I am not sure of the Python names of thing yet):

import signal

def signal_handler(signum, frame):
    raise Exception("Timeout!")

signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(3) # Three seconds
try:
    for x in range(0, 3):
        # Call function A(x)
except Exception, msg:
    print "Timeout!"
signal.alarm(0) # Reset
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mikel Pascual
  • 2,202
  • 18
  • 27
3

The comments are correct in that you should check inside. Here is a potential solution. Note that an asynchronous function (by using a thread for example) is different from this solution. This is synchronous which means it will still run in series.

import time

for x in range(0,3):
    someFunction()

def someFunction():
    start = time.time()
    while (time.time() - start < 5):
        # do your normal function

    return;
ZekeDroid
  • 7,089
  • 5
  • 33
  • 59