4

I'm trying to implement a timeout functionality in Python.

It works by wrapping functions with a function decorator that calls the function as a thread but also calls a 'watchdog' thread that will raise an exception in the function thread after a specified period has elapsed.

It currently works for threads that don't sleep. During the do_rand call, I suspect the 'asynchronous' exception is actually being called after the time.sleep call and after the execution has moved beyond the try/except block, as this would explain the Unhandled exception in thread started by error. Additionally, the error from the do_rand call is generated 7 seconds after the call (the duration of time.sleep).

How would I go about 'waking' a thread up (using ctypes?) to get it to respond to an asynchronous exception ?

Or possibly a different approach altogether ?

Code:

# Import System libraries
import ctypes
import random
import sys
import threading
import time

class TimeoutException(Exception):
    pass

def terminate_thread(thread, exc_type = SystemExit):
    """Terminates a python thread from another thread.

    :param thread: a threading.Thread instance
    """
    if not thread.isAlive():
        return

    exc = ctypes.py_object(exc_type)
    res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), exc)

    if res == 0:
        raise ValueError("nonexistent thread id")
    elif res > 1:
        # """if it returns a number greater than one, you're in trouble,
        # and you should call it again with exc=NULL to revert the effect"""
        ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
        raise SystemError("PyThreadState_SetAsyncExc failed")

class timeout_thread(threading.Thread):
    def __init__(self, interval, target_thread):
        super(timeout_thread, self).__init__()
        self.interval     = interval
        self.target_thread = target_thread
        self.done_event = threading.Event()
        self.done_event.clear()

    def run(self):
        timeout = not self.done_event.wait(self.interval)
        if timeout:
            terminate_thread(self.target_thread, TimeoutException)

class timeout_wrapper(object):
    def __init__(self, interval = 300):
        self.interval = interval

    def __call__(self, f):
        def wrap_func(*args, **kwargs):
            thread = threading.Thread(target = f, args = args, kwargs = kwargs)
            thread.setDaemon(True)
            timeout_ticker = timeout_thread(self.interval, thread)
            timeout_ticker.setDaemon(True)
            timeout_ticker.start()
            thread.start()
            thread.join()
            timeout_ticker.done_event.set()
        return wrap_func

@timeout_wrapper(2)
def print_guvnah():
    try:
        while True:
            print "guvnah"

    except TimeoutException:
        print "blimey"

def print_hello():
    try:
        while True:
            print "hello"

    except TimeoutException:
        print "Whoops, looks like I timed out"

def do_rand(*args):
    try:
        rand_num   = 7 #random.randint(0, 10)
        rand_pause = 7 #random.randint(0,  5)
        print "Got rand: %d" % rand_num
        print "Waiting for %d seconds" % rand_pause
        time.sleep(rand_pause)
    except TimeoutException:
        print "Waited too long"

print_guvnah()
timeout_wrapper(3)(print_hello)()
timeout_wrapper(2)(do_rand)()
dilbert
  • 3,008
  • 1
  • 25
  • 34

2 Answers2

4

The problem is that time.sleep blocks. And it blocks really hard, so the only thing that can actually interrupt it is process signals. But the code with it gets really messy and in some cases even signals don't work ( when for example you are doing blocking socket.recv(), see this: recv() is not interrupted by a signal in multithreaded environment ).

So generally interrupting a thread (without killing entire process) cannot be done (not to mention that someone can simply override your signal handling from a thread).

But in this particular case instead of using time.sleep you can use Event class from threading module:

Thread 1

from threading import Event

ev = Event()
ev.clear()

state = ev.wait(rand_pause) # this blocks until timeout or .set() call

Thread 2 (make sure it has access to the same ev instance)

ev.set() # this will unlock .wait above

Note that state will be the internal state of the event. Thus state == True will mean that it was unlocked with .set() while state == False will mean that timeout occured.

Read more about events here:

http://docs.python.org/2/library/threading.html#event-objects

Community
  • 1
  • 1
freakish
  • 54,167
  • 9
  • 132
  • 169
  • Thanks for the response, however, the focus of this code is to be a library (hopefully). As a result, I really don't want to constrain the user code to exclude the use of time.sleep or any other blocking function. `do_rand` is just a test. I was hoping that maybe I could modify the PyThreadState using ctypes (or similar): https://gist.github.com/gdementen/5635324#file-recursive_ctypes_struct-py. – dilbert Nov 27 '13 at 08:51
  • @dilbert As I said: system calls such as `recv()` might (and will in case of blocking `recv()`) lock your thread indefinitely. And you won't be able to interrupt it without (force) killing entire process. Thus I think that you are going to far with that, the problem can't be solved. But the question you should ask is: is that a problem in the first place? Perhaps allowing blocking entire thread is not bad at all? – freakish Nov 27 '13 at 10:06
  • On Windows, only the main thread can be interrupted from `time.sleep`. It uses `WaitForSingleObject` on an event, while all other threads use an uninterruptibile `Sleep`. The `select` call used by `time.sleep` on POSIX systems can be interrupted using `pthread_kill` to target the thread. – Eryk Sun Nov 27 '13 at 10:26
  • @freakish, that's a fair point. The idea of this code is terminate long-running tasks that violate developer expectation of execution time. – dilbert Nov 27 '13 at 10:59
  • @eryksun, on the one hand, thanks for the information but, on the other hand, that's very distressing (re the Windows non-main threads). – dilbert Nov 27 '13 at 11:01
  • @dilbert Another idea is to use subprocesses instead of threads. You can simply kill the process. – freakish Nov 27 '13 at 11:02
  • @freakish, I did consider it but I don't want the overhead of processes (especially for something that's called frequently). – dilbert Nov 27 '13 at 11:08
2

You'd need to use something other than sleep, or you'd need to send a signal to the other thread in order to make it wake up.

One option I've used is to set up a pair of file descriptors and use select or poll instead of sleep, this lets you write something to the file descriptor to wake up the other thread. Alternatively you just wear waiting until the sleep finishes if all you need is for the operation to error out because it took too long and nothing else is depending on it.

Benno
  • 5,640
  • 2
  • 26
  • 31
  • How would I go about sending the thread a signal ? And which signal? – dilbert Nov 27 '13 at 05:56
  • The `signal` module will do it. The file descriptor method is probably more reliable though. – Benno Nov 27 '13 at 05:57
  • I very much wish to avoid use of files. So there isn't anything particular about the signal, as in, any will do? – dilbert Nov 27 '13 at 05:58
  • Well I'd avoid SIGTERM and SIGKILL. =) You may need to add a signal handler to avoid having the signal kill your process off. You're relying on the signal delivery interrupting the sleep on the thread you're expecting, too. This may not hold across all OSes. – Benno Nov 27 '13 at 05:59
  • I am aiming for cross-platform. I was hoping for some means of manipulating PyThreadState using ctypes. I suppose SIGINT would be alright although the `signal` documentation does mention to be cautious of using both threads and signals in a program. – dilbert Nov 27 '13 at 06:06
  • 2
    Your problem is that the sleep stops the thread from running so it doesn't see the request to raise the exception until it comes out of the sleep. If you want an interruptible sleep you need to use the select/poll mechanism I mentioned or you could look into using a condition variable or suchlike. See the `threading` module. – Benno Nov 27 '13 at 06:07