Created
September 13, 2013 15:47
-
-
Save psobot/6552424 to your computer and use it in GitHub Desktop.
A terminatable thread class that automatically joins all threads with the main thread on exit. Based on work by Tomer Filiba (http://tomerfiliba.com/recipes/Thread2/) but updated and tested on Python 2.7.1.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import atexit | |
import ctypes | |
import inspect | |
import threading | |
""" | |
Any instances of TerminatableThread that are running when the program exits | |
(due to normal interpreter shutdown, KeyboardInterrupt, SystemExit, etc.) | |
will have SystemExit raised within their next 100 Python bytecode executions, | |
allowing them to possibly catch the SystemExit and clean up as necessary. | |
""" | |
def _async_raise(tid, exctype): | |
"""raises the exception, performs cleanup if needed""" | |
if not inspect.isclass(exctype): | |
raise TypeError("Only types can be raised (not instances)") | |
res = ctypes.pythonapi.PyThreadState_SetAsyncExc( | |
ctypes.c_long(tid), ctypes.py_object(exctype) | |
) | |
if res == 0: | |
raise ValueError("invalid 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(tid, 0) | |
raise SystemError("PyThreadState_SetAsyncExc failed") | |
class TerminatableThread(threading.Thread): | |
""" | |
Base class that threads can use to be stopped by something | |
on the main thread - e.g.: a signal handler, an IOLoop event, | |
or something like that. When terminate() is called on the thread, | |
or terminate_all() is called on the class, all threads will have | |
SystemExit raised before executing the next line of Python bytecode. | |
Based on work by Tomer Filiba: http://tomerfiliba.com/recipes/Thread2/ | |
""" | |
all_threads = [] | |
registered = False | |
def __init__(self, *args, **kwargs): | |
threading.Thread.__init__(self, *args, **kwargs) | |
self.setDaemon(True) | |
def start(self, *args, **kwargs): | |
self.all_threads.append(self) | |
if not self.registered: | |
atexit.register(self.terminate_all) | |
self.registered = True | |
threading.Thread.start(self, *args, **kwargs) | |
def raise_exc(self, exctype): | |
"""raises the given exception type in the context of this thread""" | |
_async_raise(self.ident, exctype) | |
def terminate(self): | |
"""raises SystemExit in the context of the given thread, which should | |
cause the thread to exit silently (unless caught)""" | |
self.raise_exc(SystemExit) | |
@classmethod | |
def terminate_all(self, *args, **kwargs): | |
""" | |
Stops all registered threads in the order that they | |
were started. | |
""" | |
for thread in self.all_threads: | |
if thread.is_alive(): | |
thread.terminate() | |
thread.join() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment