PyQT - QThread.sleep(...)在单独的线程块UI

时间:2016-03-31 22:39:56

标签: multithreading qt pyqt4 qthread worker

首先,在深入讨论问题和代码之前,我将简要介绍用户界面及其功能。提前抱歉,但我无法提供完整的代码(即使我可以...很多行:D)。

用户界面及其功能的说明 我有一个自定义QWidget或更精确的N个实例,该自定义窗口小部件在网格布局中对齐。窗口小部件的每个实例都有自己的QThread,其中包含工作人员QObjectQTimer。在UI组件方面,小部件包含两个重要组件 - 一个可视化状态的QLabel和一个QPushButton,它可以启动(通过触发Worker中的start()槽)或停止(通过触发工作中的slot()槽)外部进程。两个插槽都包含5秒延迟,并在执行期间禁用按钮。工作者本身不仅控制外部进程(通过调用上面提到的两个插槽),还检查进程是否由status()插槽运行,该插槽由QTimer每1s触发。如上所述,工作者和计时器都在线程内部! (我通过打印主要(UI所在的位置)和每个工作者(不同于主要100%确定)的线程ID进行了双重检查。

为了减少从UI到工作人员的呼叫量,反之亦然,我决定声明_status属性(保持外部进程的状态 - 无效,我的Worker课程正在运行错误)为Q_PROPERTYsettergetter和{{1}最后一个是从notify IF内触发的信号,并且只有当值从旧值改变时才会触发。我之前的设计是更多的信号/插槽密集型,因为状态是每秒发出的。

现在是时候换一些代码了。我已将代码仅减少到我认为提供足够信息的部分以及出现问题的位置:

在QWidget内

setter

工作者内部

# ...

def createWorker(self):
    # Create thread
    self.worker_thread = QThread()

    # Create worker and connect the UI to it
    self.worker = None
    if self.pkg: self.worker = Worker(self.cmd, self.pkg, self.args)
    else: self.worker = Worker(cmd=self.cmd, pkg=None, args=self.args)

    # Trigger attempt to recover previous state of external process
    QTimer.singleShot(1, self.worker.recover)

    self.worker.statusChanged_signal.connect(self.statusChangedReceived)
    self.worker.block_signal.connect(self.block)
    self.worker.recover_signal.connect(self.recover)
    self.start_signal.connect(self.worker.start)
    self.stop_signal.connect(self.worker.stop)
    self.clear_error_signal.connect(self.worker.clear_error)

    # Create a timer which will trigger the status slot of the worker every 1s (the status slot sends back status updates to the UI (see statusChangedReceived(self, status) slot))
    self.timer = QTimer()
    self.timer.setInterval(1000)
    self.timer.timeout.connect(self.worker.status)

    # Connect the thread to the worker and timer
    self.worker_thread.finished.connect(self.worker.deleteLater)
    self.worker_thread.finished.connect(self.timer.deleteLater)
    self.worker_thread.started.connect(self.timer.start)

    # Move the worker and timer to the thread...
    self.worker.moveToThread(self.worker_thread)
    self.timer.moveToThread(self.worker_thread)

    # Start the thread
    self.worker_thread.start()

@pyqtSlot(int)
  def statusChangedReceived(self, status):
    '''
    Update the UI based on the status of the running process
    :param status - status of the process started and monitored by the worker
    Following values for status are possible:
      - INACTIVE/FINISHED - visual indicator is set to INACTIVE icon; this state indicates that the process has stopped running (without error) or has never been started
      - RUNNING - if process is started successfully visual indicator
      - FAILED_START - occurrs if the attempt to start the process has failed
      - FAILED_STOP - occurrs if the process wasn't stop from the UI but externally (normal exit or crash)
    '''
    #print(' --- main thread ID: %d ---' % QThread.currentThreadId())
    if status == ProcStatus.INACTIVE or status == ProcStatus.FINISHED:
      # ...
    elif status == ProcStatus.RUNNING:
      # ...
    elif status == ProcStatus.FAILED_START:
      # ...
    elif status == ProcStatus.FAILED_STOP:
      # ...

@pyqtSlot(bool)
  def block(self, block_flag):
    '''
    Enable/Disable the button which starts/stops the external process
    This slot is used for preventing the user to interact with the UI while starting/stopping the external process after a start/stop procedure has been initiated
    After the respective procedure has been completed the button will be enabled again
    :param block_flag - enable/disable flag for the button
    '''
    self.execute_button.setDisabled(block_flag)


# ...

现在关于我的问题:我注意到只有在# ... @pyqtSlot() def start(self): self.block_signal.emit(True) if not self.active and not self.pid: self.active, self.pid = QProcess.startDetached(self.cmd, self.args, self.dir_name) QThread.sleep(5) # Check if launching the external process was successful if not self.active or not self.pid: self.setStatus(ProcStatus.FAILED_START) self.block_signal(False) self.cleanup() return self.writePidToFile() self.setStatus(ProcStatus.RUNNING) self.block_signal.emit(False) @pyqtSlot() def stop(self): self.block_signal.emit(True) if self.active and self.pid: try: kill(self.pid, SIGINT) QThread.sleep(5) # <----------------------- UI freezes here except OSError: self.setStatus(ProcStatus.FAILED_STOP) self.cleanup() self.active = False self.pid = None self.setStatus(ProcStatus.FINISHED) self.block_signal.emit(False) @pyqtSlot() def status(self): if self.active and self.pid: running = self.checkProcessRunning(self.pid) if not running: self.setStatus(ProcStatus.FAILED_STOP) self.cleanup() self.active = False self.pid = None def setStatus(self, status): if self._status == status: return #print(' --- main thread ID: %d ---' % QThread.currentThreadId()) self._status = status self.statusChanged_signal.emit(self._status) 插槽被触发并且代码的执行通过stop()时,UI才会冻结。我认为这也应该影响启动但是我的小部件的多个实例(每个实例控制自己的线程,其中有一个工作者和计时器)并且所有运行启动的按预期工作 - 按钮,用于触发QThread.sleep(5)start()个插槽将被禁用5秒钟然后启用。在stop()被触发时,这根本不会发生。

我真的无法解释这种行为。更糟糕的是,我通过stop()设置器Q_PROPERTY发出的状态更新由于此冻结而延迟,导致我的self.setStatus(...)功能的一些额外调用基本上删除生成的文件。

知道这里发生了什么吗?时隙和信号的本质是一旦发出信号,就立即调用连接到它的插槽。由于用户界面在不同的线程中运行,因此工作人员不知道为什么会发生这种情况。

1 个答案:

答案 0 :(得分:0)

我实际上在我的问题中纠正了问题所在的地方。在我的原始代码中,我忘记了@ pyqtSlot()函数stop()之前的class Dashing.Number4 extends Dashing.Widget @accessor 'current', Dashing.AnimatedValue @accessor 'difference', -> if @get('last') last = parseInt(@get('last')) current = parseInt(@get('current')) if last != 0 diff = Math.abs(Math.round((current - last) / last * 100)) "#{diff}%" else "" @accessor 'arrow', -> if @get('last') if parseInt(@get('current')) > parseInt(@get('last')) then 'fa fa-arrow-up' else 'fa fa-arrow-down' constructor: -> super @onData(Dashing.lastEvents[@id]) if Dashing.lastEvents[@id] onData: (data) -> if parseInt(@get('current')) > parseInt(@get('last')) then $(@node).css('background-color', '#006600') else $(@node).css('background-color', '#660000') 。添加后,它完全正常。我没有想到这样的事情会导致如此巨大的问题!