PySide/PyQt - запуск интенсивного потока процессора зависает всего приложения

Я пытаюсь сделать довольно обычное дело в моем приложении PySide GUI: я хочу делегировать некоторую задачу CPU-Intensive в фоновый поток, чтобы мой графический интерфейс оставался отзывчивым и даже отображал индикатор прогресса по мере того, как вычисляется.

Вот что я делаю (я использую PySide 1.1.1 на 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()

Приложение отображает одно окно с кнопкой. Когда кнопка нажата, я ожидаю, что она отключится, пока выполняется вычисление. Затем нужно снова включить кнопку.

Вместо этого происходит то, что при нажатии кнопки все окно зависает во время вычисления, а затем, когда оно закончится, я снова получаю контроль над приложением. Кнопка никогда не отключается. Самое смешное, что я заметил, это то, что если я заменю интенсивное вычисление CPU в do_stuff() простым прообразом time.sleep(), программа будет вести себя так, как ожидалось.

Я точно не знаю, что происходит, но кажется, что приоритет второго потока настолько высок, что он фактически предотвращает планирование графика графического интерфейса. Если второй поток переходит в состояние BLOCKED (как это происходит с sleep()), GUI имеет возможность запускать и обновлять интерфейс, как ожидалось. Я попытался изменить приоритет рабочего потока, но похоже, что это невозможно сделать в Linux.

Кроме того, я пытаюсь распечатать идентификаторы потоков, но я не уверен, правильно ли я делаю это. Если я, то сродство нитей кажется правильным.

Я также пробовал программу с PyQt, и поведение было точно таким же, следовательно, теги и заголовок. Если я могу заставить его работать с PyQt4 вместо PySide, я мог бы переключить все свое приложение на PyQt4

Ответ 1

Это, вероятно, вызвано тем, что рабочий поток поддерживает Python GIL. В некоторых реализациях Python может выполняться только один поток Python. GIL предотвращает выполнение другими потоками кода Python и освобождается во время вызовов функций, которым не нужен GIL.

Например, GIL освобождается во время фактического ввода-вывода, поскольку IO обрабатывается операционной системой, а не интерпретатором Python.

Решения:

  • По-видимому, вы можете использовать time.sleep(0) в своем рабочем потоке для перехода к другим потокам (в соответствии с этим вопросом SO). Вам придется периодически вызывать time.sleep(0) самостоятельно, и поток GUI будет работать только тогда, когда фоновый поток вызывает эту функцию.

  • Если рабочий поток достаточно автономный, вы можете поместить его в совершенно отдельный процесс, а затем обмениваться сообщениями, отправив маринованные объекты по трубам. В процессе переднего плана создайте рабочий поток для выполнения ввода-вывода с фоновым процессом. Поскольку рабочий поток будет выполнять IO вместо операций с ЦП, он не будет удерживать GIL, и это даст вам полностью отзывчивый поток графического интерфейса.

  • Некоторые реализации Python (JPython и IronPython) не имеют GIL.

Темы в CPython действительно полезны только для мультиплексирования операций ввода-вывода, а не для задания задач с интенсивным использованием процессора в фоновом режиме. Для многих приложений потоки в реализации CPython принципиально нарушены, и это, вероятно, останется таким в обозримом будущем.

Ответ 2

в конце это работает для моей проблемы - так может ли код помочь кому-то другому.

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_())