Nano Hash - криптовалюты, майнинг, программирование

PySide/PyQt — запуск потока с интенсивным использованием ЦП приводит к зависанию всего приложения.

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

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

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

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

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

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

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


Ответы:


1

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

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

Решения:

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

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

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

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

29.06.2012
  • Я попытался поставить time.sleep(0) вместо комментария time.sleep(0.2) в примере и, к сожалению, не решил проблему. Я заметил, что с такими значениями, как time.sleep(0.05), кнопка ведет себя так, как ожидалось. В качестве обходного пути я мог бы настроить worker так, чтобы он сообщал о прогрессе только несколько раз в секунду и переходил в спящий режим, чтобы графический интерфейс обновлялся сам. 29.06.2012
  • Существуют обходные пути для потоков и графических интерфейсов (например, code.activestate.com/recipes/578154). . 29.06.2012
  • В итоге я сделал так: я поставил sleep там, где начинается вычисление, и каждый раз, когда индикатор прогресса обновляется, я вызываю app.processEvents(), чтобы заставить работать кнопку «Прервать». 30.06.2012
  • Это в корне неверно на всех возможных уровнях: Threads in CPython are only really useful for multiplexing IO operations, not for putting CPU-intensive tasks in the background. Нет Приложение Python на основе Qt, выполняющее задачу с интенсивным использованием ЦП в фоновом режиме, тривиально реализуемо. Почему? Потому что основной поток GUI тратит подавляющую часть своего временного интервала на чистом уровне C++ Qt, таким образом, обходя GIL Python. Графический интерфейс должен всегда реагировать в приложениях Python на основе Qt. Если это не так, вы случайно запускаете задачу с интенсивным использованием ЦП в основном потоке графического интерфейса. 31.03.2018
  • @CecilCurry: «Фундаментально неправильно на всех возможных уровнях»… я убил вашу собаку или что-то в этом роде? Каждый раз, когда вы обрабатываете задачу с интенсивным использованием ЦП в фоновом режиме, существует риск того, что вы не сможете выпустить GIL. Если вы используете Qt в своем приложении Python, код C++ все равно должен получить GIL при обратном вызове Python. Тот факт, что Qt проводит большую часть своего времени без GIL, не имеет значения, если ему нужно вернуть GIL всякий раз, когда вы действительно что-то делаете. 31.03.2018

  • 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_())
    
    08.10.2016
    Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

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

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..