PySide/PyQt - Starting a CPU intensive thread hangs the whole application
Asked Answered
L

2

16

I'm trying to do a fairly common thing in my PySide GUI application: I want to delegate some CPU-Intensive task to a background thread so that my GUI stays responsive and could even display a progress indicator as the computation goes.

Here is what I'm doing (I'm using PySide 1.1.1 on Python 2.7, Linux x86_64):

import sys
import time
from PySide.QtGui import QMainWindow, QPushButton, QApplication, QWidget
from PySide.QtCore import QThread, QObject, Signal, Slot

class Worker(QObject):
    done_signal = Signal()

    def __init__(self, parent = None):
        QObject.__init__(self, parent)

    @Slot()
    def do_stuff(self):
        print "[thread %x] computation started" % self.thread().currentThreadId()
        for i in range(30):
            # time.sleep(0.2)
            x = 1000000
            y = 100**x
        print "[thread %x] computation ended" % self.thread().currentThreadId()
        self.done_signal.emit()


class Example(QWidget):

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

        self.initUI()

        self.work_thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.work_thread)
        self.work_thread.started.connect(self.worker.do_stuff)
        self.worker.done_signal.connect(self.work_done)

    def initUI(self):

        self.btn = QPushButton('Do stuff', self)
        self.btn.resize(self.btn.sizeHint())
        self.btn.move(50, 50)       
        self.btn.clicked.connect(self.execute_thread)

        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Test')    
        self.show()


    def execute_thread(self):
        self.btn.setEnabled(False)
        self.btn.setText('Waiting...')
        self.work_thread.start()
        print "[main %x] started" % (self.thread().currentThreadId())

    def work_done(self):
        self.btn.setText('Do stuff')
        self.btn.setEnabled(True)
        self.work_thread.exit()
        print "[main %x] ended" % (self.thread().currentThreadId())


def main():

    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

The application displays a single window with a button. When the button is pressed, I expect it to disable itself while the computation is performed. Then, the button should be re-enabled.

What happens, instead, is that when I press the button the whole window freezes while the computation goes and then, when it's finished, I regain control of the application. The button never appears to be disabled. A funny thing I noticed is that if I replace the CPU intensive computation in do_stuff() with a simple time.sleep() the program behaves as expected.

I don't exactly know what's going on, but it appears that the second thread's priority is so high that it's actually preventing the GUI thread from ever being scheduled. If the second thread goes in BLOCKED state (as it happens with a sleep()), the GUI has actually the chance to run and updates the interface as expected. I tried to change the worker thread priority, but it looks like it can't be done on Linux.

Also, I try to print the thread IDs, but I'm not sure if I'm doing it correctly. If I am, the thread affinity seems to be correct.

I also tried the program with PyQt and the behavior is exactly the same, hence the tags and title. If I can make it run with PyQt4 instead of PySide I could switch my whole application to PyQt4

Librarianship answered 29/6, 2012 at 16:47 Comment(0)
T
16

This is probably caused by the worker thread holding Python's GIL. In some Python implementations, only one Python thread can execute at a time. The GIL prevents other threads from executing Python code, and is released during function calls that don't need the GIL.

For example, the GIL is released during actual IO, since IO is handled by the operating system and not the Python interpreter.

Solutions:

  1. Apparently, you can use time.sleep(0) in your worker thread to yield to other threads (according to this SO question). You will have to periodically call time.sleep(0) yourself, and the GUI thread will only run while the background thread is calling this function.

  2. If the worker thread is sufficiently self-contained, you can put it into a completely separate process, and then communicate by sending pickled objects over pipes. In the foreground process, create a worker thread to do IO with the background process. Since the worker thread will be doing IO instead of CPU operations, it won't hold the GIL and this will give you a completely responsive GUI thread.

  3. Some Python implementations (JPython and IronPython) do not have a GIL.

Threads in CPython are only really useful for multiplexing IO operations, not for putting CPU-intensive tasks in the background. For many applications, threading in the CPython implementation is fundamentally broken and it is likely to stay that way for the forseeable future.

Technology answered 29/6, 2012 at 17:3 Comment(5)
I tried putting time.sleep(0) instead of the time.sleep(0.2) comment in the example and, unfortunately, doesn't fix the problem. I noticed that with values such as time.sleep(0.05) the button behaves as expected. As a workaround, I could set up the worker to report progress only a few times per second and sleep to let the GUI update itself.Librarianship
There are workarounds for threads and GUIs (such as code.activestate.com/recipes/578154).Gelasius
I ended up doing like this: I put a sleep where the computation begins, and every time the progress indicator is updated I call app.processEvents() in order to make an "Abort" button work.Librarianship
This is fundamentally wrong on all possible levels: Threads in CPython are only really useful for multiplexing IO operations, not for putting CPU-intensive tasks in the background. No. A Qt-based Python application running a CPU-intensive task in the background is trivially implementable. Why? Because the main GUI thread spends the overwhelming majority of its time slice at the pure-C++ Qt layer, thus circumventing Python's GIL. The GUI should always be responsive in Qt-based Python applications. If it isn't, you're accidentally running your CPU-intensive task in the main GUI thread.Theriault
@CecilCurry: “Fundamentally wrong on all possible levels”… did I kill your dog or something? Any time you’re handling a CPU intensive task in the background there’s a risk that you might not release the GIL. If you’re using Qt in your Python application, the C++ code still has to get the GIL when it calls back into Python. The fact that Qt spends most of its time without the GIL is irrelevant if it needs to get the GIL back whenever you actually do something.Technology
C
0

at the end this works for my problem - so may the code help someone else.

import sys
from PySide import QtCore, QtGui
import time

class qOB(QtCore.QObject):

    send_data = QtCore.Signal(float, float)

    def __init__(self, parent = None):
        QtCore.QObject.__init__(self)
        self.parent = None
        self._emit_locked = 1
        self._emit_mutex = QtCore.QMutex()

    def get_emit_locked(self):
        self._emit_mutex.lock()
        value = self._emit_locked
        self._emit_mutex.unlock()
        return value

    @QtCore.Slot(int)
    def set_emit_locked(self, value):
        self._emit_mutex.lock()
        self._emit_locked = value
        self._emit_mutex.unlock()

    @QtCore.Slot()
    def execute(self):
        t2_z = 0
        t1_z  = 0
        while True:
            t = time.clock()

            if self.get_emit_locked() == 1: # cleaner
            #if self._emit_locked == 1: # seems a bit faster but less               responsive, t1 = 0.07, t2 = 150
                self.set_emit_locked(0)
                self.send_data.emit((t-t1_z)*1000, (t-t2_z)*1000)
                t2_z = t

            t1_z = t

class window(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)

        self.l = QtGui.QLabel(self)
        self.l.setText("eins")

        self.l2 = QtGui.QLabel(self)
        self.l2.setText("zwei")

        self.l2.move(0, 20) 

        self.show()

        self.q = qOB(self)
        self.q.send_data.connect(self.setLabel)

        self.t = QtCore.QThread()
        self.t.started.connect(self.q.execute)
        self.q.moveToThread(self.t)

        self.t.start()

    @QtCore.Slot(float, float)
    def setLabel(self, inp1, inp2):

        self.l.setText(str(inp1))
        self.l2.setText(str(inp2))

        self.q.set_emit_locked(1)



if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    win = window()
    sys.exit(app.exec_())
Colman answered 8/10, 2016 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.