Making Good On Your Threads

If you’re a TD in the animation industry, you have, or will, write tools using PyQt, PySide, or the Qt.py shim around them. And you will run into the situation where the cats you herd artists you support are complaining that a tool is too slow. Something it does like querying the database or moving files takes more than a few seconds. Eventually, one of the artists makes a statement in the passive aggressive subjunctive tense.

“It’d be great if this [slow operation] could run faster.”

You try to explain to the artist that you can’t change the laws of physics that govern the speed of the operation. And then they will artist-splain that, “At my last job…”

Enter QThreads.

Stay calm. They can probably smell fear.

Even though Python doesn’t do threads as well as other programming languages because of the infamous GIL (Global Interpreter Lock), you can still use them to prevent blocking I/O from locking up a UI. You use QThreads instead of native Python threads because it lets you do things like emit signals back.

Consider the following example.

EDIT: After running into a problem with QTimer crashing Maya, I went digging for information and found a very good blog post that re-iterates the official docs. Because, as a wise man once said, “Yeah, you know, you really should have stolen the whole book because the warnings… The warnings come *after* the spells.”

The blog post is here. The official docs are here. Yes, those are the C++ docs. Be not afraid.

Due to this revelations, I’ve updated the example with the thread/worker model that is suggested.

"""Test program to demonstrate running a process in the background."""

import functools
import random
import subprocess  
import sys
import time 

from PySide import QtCore


class BackgroundProcess(QtCore.QObject):
    """Monitor a process running in the background."""

    Finished = QtCore.Signal()
    Timeout = QtCore.Signal()

    def __init__(self, cmd, timeout=10.0):
        super(BackgroundProcess, self).__init__()

        self.cmd = cmd 
        self.timeout = timeout

        self.start_time = 0.0

    def run(self):
        process = subprocess.Popen(self.cmd, shell=True)
        print("[{}] {}".format(process.pid, self.cmd))

        self.start_time = time.time()

        while True:
            elapsed_time = time.time() - self.start_time

            if elapsed_time > self.timeout:
                self.Timeout.emit()
                break

            if process.poll() is not None:
                self.Finished.emit()
                break


class MyProgram(QtCore.QObject):
    """Test program to demonstrate running a process on a background thread."""

    def __init__(self):
        super(MyProgram, self).__init__()

        self.run_timer = QtCore.QTimer()
        self.run_timer.timeout.connect(functools.partial(self.tick))
        
        delay = random.randint(2, 5)
        cmd = 'ping -n {} 127.0.0.1 > nul'.format(delay)

        self.start_time = time.time()
        
        self.background_thread = QtCore.QThread()

        self.background_process = BackgroundProcess(cmd, timeout=5.0)
        self.background_process.moveToThread(self.background_thread)

        self.background_process.Finished.connect(self.background_thread.quit)
        self.background_process.Timeout.connect(self.background_thread.quit)
        self.background_process.Finished.connect(self.on_work_finished)
        self.background_process.Timeout.connect(self.on_work_timeout)

        self.background_thread.finished.connect(self.background_thread.deleteLater)
        self.background_thread.started.connect(self.background_process.run)

        self.background_thread.start() 
        self.run_timer.start(100)

    def tick(self):                   
        elapsed_time = time.time() - self.start_time
        print("Elapsed time: {:.1f} seconds.".format(round(elapsed_time, 2)))

    def on_work_finished(self):
        print("Work complete")
        self.exit()

    def on_work_timeout(self):
        print("Timeout error")
        self.exit()

    def exit(self):   
        self.run_timer.stop()

        # Wait for the thread to fully stop
        self.background_thread.wait()

        QtCore.QCoreApplication.instance().exit(0)           


def main():
    app = QtCore.QCoreApplication(sys.argv)
    prog = MyProgram()  
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

This program does … basically nothing. It pings your local host between three and seven times, waiting one second between each ping. If it runs for more than five seconds, it times out. If you ran this process on the main thread, it would block the whole time. Imagine if it took thirty seconds to run?

Because the BackgroundProcess is running on a separate thread, it does not interfere with the main thread. I ran this code in Maya (with the invocation of a new QCoreApplication removed) and it happily ticked away in the background while I played back animation in my scene.

Using threads isn’t without its risks. But threads can make your tools run “faster”, from the artist’s point of view.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s