Completely random side-note: I think this is one of my new favorite python patterns (I just learned about decorators recently). There's a few places to use it in Armory, but mostly excited about for other applications.
class PyBackgroundThread(threading.Thread):
"""
Wraps a function in a threading.Thread object which will run
that function in a separate thread. Calling self.start() will
return immediately, but will start running that function in
separate thread. You can check its progress later by using
self.isRunning() or self.isFinished(). If the function returns
a value, use self.getOutput(). Use self.getElapsedSeconds()
to find out how long it took.
"""
def __init__(self, *args, **kwargs):
threading.Thread.__init__(self)
self.output = None
self.startedAt = UNINITIALIZED
self.finishedAt = UNINITIALIZED
if len(args)==0:
self.func = lambda: ()
else:
if not hasattr(args[0], '__call__'):
raise TypeError, ('PyBkgdThread constructor first arg '
'(if any) must be a function')
else:
self.setThreadFunction(args[0], *args[1:], **kwargs)
def setThreadFunction(self, thefunc, *args, **kwargs):
def funcPartial():
return thefunc(*args, **kwargs)
self.func = funcPartial
def isFinished(self):
return not (self.finishedAt==UNINITIALIZED)
def isStarted(self):
return not (self.startedAt==UNINITIALIZED)
def isRunning(self):
return (self.isStarted() and not self.isFinished())
def getElapsedSeconds(self):
if not self.isFinished():
LOGERROR('Thread is not finished yet!')
return None
else:
return self.finishedAt - self.startedAt
def getOutput(self):
if not self.isFinished():
if self.isRunning():
LOGERROR('Cannot get output while thread is running')
else:
LOGERROR('Thread was never .start()ed')
return None
return self.output
def start(self):
# The prefunc is blocking. Probably preparing something
# that needs to be in place before we start the thread
self.startedAt = RightNow()
super(PyBackgroundThread, self).start()
def run(self):
# This should not be called manually. Only call start()
self.output = self.func()
self.finishedAt = RightNow()
# Define a decorator that allows the function to be called asynchronously
def AllowAsync(func):
def wrappedFunc(*args, **kwargs):
if not 'async' in kwargs or not kwargs['async']==True:
# Run the function normally
if 'async' in kwargs:
del kwargs['async']
return func(*args, **kwargs)
else:
# Run the function as a background thread
del kwargs['async']
thr = PyBackgroundThread(func, *args, **kwargs)
thr.start()
return thr
return wrappedFunc
Simply take any function that you would normally define,
def myFunc(...):
doSomething()
And add:
@AllowAsync
def myFunc(...):
doSomething()
You can now call myFunc(..., async=True) to have it run in the background instead of in the main thread (control will go to the next line of code immediately without wainting for myFunc to finish). If you want to keep track of it, you can instead do:
thr = myFunc(..., async=True)
while not thr.isFinished():
doOtherStuff()
# It must be finished to have gotten here
data = thr.getOutput()
print "myFunc took %f seconds" % thr.getElapsedSeconds()
If you have functions that do a lot of I/O, but aren't needed for the subsequent operations, you can simply do the following to parallelize:
thr = myFunc(..., async=True)
doOtherStuffInParallel()
thr.join() # will wait for it to finish
Very cool! Just keep in mind that you don't get a computational advantage using python threads, but if you are doing things that are I/O limited, networking, UI-related, etc... it works wonderfully.
Okay, now back to this orphan chain bug...