Python qt: threading

From wikinotes

Before starting here, read the helpful introductions provided by Qt:



Strategies

Worker QObjects

This is really cool - so far it is the best way I have seen of manipulating state within a thread. The idea is that you create a QThread, and a QObject that will have all of the methods that can run within that thread. All of that object's state will be contained within that thread, you can use/reuse the thread, and you can freely communicate with the thread from other threads. The other advantage here is that you absolutely cannot share state with the GUI thread - just data - it also makes it very clear exatly what jobs are being performed within the thread.

  • calling a worker's method directly will wait in the main thread for it to finish. Code is still run in the thread, but it blocks the UI thread.
  • calling a worker's method by signal starts it truly asynchronously.
  • threads can only run one method at a time. this is useful to keep gui responsive, while still running more-or-less synchronous code. No mutexes required.


class Window(QtWidgets.QPushButton):
    def __init__(self):
        QtWidgets.QPushButton.__init__(self, 'Run Asynchronous Job')

        print('main thread:   %s' % QtCore.QThread.currentThread() )
        self._worker  = Worker()
        self._thread  = QtCore.QThread()
        self._worker.moveToThread( self._thread )
        self._thread.start()

        self.clicked.connect( self.do_something )

    def do_something(self):
        self._worker.do_something( wait=True )
        print('worker queued')


class Worker( QtCore.QObject ):
    _method__do_something = QtCore.Signal()
    def __init__(self):
        QtCore.QObject.__init__(self)
        self._method__do_something.connect( self._do_something )

    def do_something(self, wait=False ):
        if wait:
            self._do_something()
        else:
            self._method__do_something.emit()

    def _do_something(self):
        print('worker thread: %s' % QtCore.QThread.currentThread() )
        for i in range(10000000):
            var = '%s' % i
        print('thread finished')
https://stackoverflow.com/questions/16246796/how-to-stop-a-qthread-from-the-gui see answers

QRunnable/QThreadPool

The simplest, and probably safest way to make your program threaded is to create subclasses of QRunnable, and then run them using the QApplication's QThreadPool. It will automatically be managed, created, destroyed, and limit the amount of simultaneous threads in the mix at any given time.

class MyJob( QtCore.QRunnable ):
    def run(self):
        print( QtCore.QThread.currentThread() )      # shows the thread a QRunnable is running in

Note that QRunnables cannot have custom signals.

http://nullege.com/codes/show/src@o@r@Orange-2.7.2@Orange@OrangeWidgets@OWConcurrent.py/20/PyQt4.QtCore.QEventLoop/python use of qeventloop within qrunnable

QThreads

The official way to run threads in PySide is to create a subclass of QtCore.QThread() for every job. It's run method is designed to be overridden with your instructions to be carried out in the new thread. you can run it with start. If the thread is deleted before it completes (goes out of scope), your application will crash. If storing the thread as an attribute, you should probably protect access to it with a mutex.

class MyJob( QtCore.QThread ):
	def run(self):
		print('running a very long job...')
		time.sleep(5)

thread = MyJob()
thread.start()

print('running other tasks while waiting for thread to complete')

Unavailable Strategies

While sussing out how threads work, I encountered a lot of much more friendly ways of working with threads that are not available to the python Qt bindings.

QtConcurrent

In C++, this magically hides all of the threading primitives, allowing you to run a method concurrently.

QMetaObject.invokeMethod

Call a method (on a worker object in a different thread) as if it were by a signal. This starts runs the method truly asynchronously, as if it were called by a signal.

Technically this exists within the bindings, but I have not been able to get it to work, and stack-overflow commentors indicated how the python bindings are overlaid on top of C++, and that this couldn't work (unless it was a slot built in to the original C++ QObject).

Utils

Mutexes (locks)

Section Marked as Requiring Revision:

This is an ugly, very case-specific example. This should be redone with less library-specific classes

#!/usr/bin/env python2


from PySide import QtCore, QtGui
from libpyside import newUI, pysideTools
import time


class Button( newUI.NewWindow ):
    def __init__(self):
        super( Button, self ).__init__()
        self._mutex = QtCore.QMutex()

    def main(self):
        layout = QtGui.QVBoxLayout()
        button = QtGui.QPushButton('push me')
        layout.addWidget( button )
        self.setLayout(layout)
        button.clicked.connect( self._handle_click )

    def _handle_click(self):
        if self._mutex.tryLock(0.5):
            print 'unlocked'
            self._thread = pysideTools.QThreadCustom( self._long_process )
            self._thread.start()
        else:
            print 'ignored'

    def _long_process(self):
        print 'starting proces...'
        time.sleep(5)
        print 'hihihihihi'
        self._mutex.unlock()

if __name__ == '__main__':
    from libpyside import newUI
    with newUI.WithQApp():
        win = Button()
        win.display()

Gotchas

max threads

Operating Systems have restrictions on how many threads can be started at once. Exceeding this will break your application, or other running applications. Be conservative with the number of threads you start.

If you are only trying to separte business logic from a GUI, I suggest reusing the same thread. All code will continue to run synchronously (only one job at a time) within that thread, you'll be less likely to encounter issues like race conditions.

If you do want to run several jobs at once, I would recommend using a QThreadPool, packing your logic into a QRunnable (and dealing with it's limitations), or emulating this using a fixed number of threads, and a queue.

thread affinity

When passing arguments to a thread, they are serialized, and recreated within the thread. Do not expect objects to remain able to communicate.

The exception to the rule is QObjects, whose thread affinity can be changed.

  • if you create a qobject inside a thread, be explicit about setting it's parent to the current thread (just to be safe)
  • if you create a qobject outside of a thread, change it's thread affinity before trying to use it in the thread.

file descriptors

File Descriptors are not threadsafe. You cannot share an open file with a thread as an argument, then use it within the thread.

subprocesses in threads

Subprocesses *can* be used in threads, but since stdin/stdout/stderr are file-descriptors, they must not be used within a thread. If you do use them within a thread, the results vary. Sometimes you may encounter crashes, other times you may not.

class WorkerObject(QtCore.QObject):
    def run_job(self):
        subprocess.check_call(
            ['netstat', '-an'],
            close_fds=True,
            stdin=None,
            stdout=None,
            stderr=None,
        )

Note that Qt also exposes it's own QtCore.QProcess class, which may or may not address this issue.