代码之家  ›  专栏  ›  技术社区  ›  rbaleksandar

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

  •  2
  • rbaleksandar  · 技术社区  · 8 年前

    首先,在深入研究问题和代码之前,我将简要描述用户界面及其功能。很抱歉,我无法提供完整的代码(即使我能提供,也有很多行:D)。

    UI及其功能的描述 我有一个习惯 QWidget 或者更精确地说是在网格布局中对齐的定制控件的N个实例。小部件的每个实例都有自己的 QThread 里面有一个工人 QObject QTimer 。就UI组件而言,小部件包含两个重要组件- QLabel 其可视化状态和 QPushButton ,其启动(通过触发 start() Worker中的插槽)或停止(通过触发 slot() 工人中的插槽)外部进程。两个插槽都包含5秒延迟,并且在执行期间禁用按钮。worker本身不仅控制外部进程(通过调用上面提到的两个插槽),还检查进程是否由 status() 插槽,由 Q计时器 每1s。如前所述,worker和timer都位于线程内!(我通过打印main的线程ID(UI所在的位置)和每个worker的线程ID进行了双重检查(与main完全不同)。

    为了减少从UI到worker的调用量,反之亦然,我决定声明 _status 属性(保存外部进程的状态- 不活跃的 , 跑步 , 错误 )我的 Worker 分类为 Q_PROPERTY 用一个 setter , getter notify 最后一个是从内部触发的信号 设置器 并且仅当该值已从旧值改变时。我以前的设计是信号/插槽密集型的,因为状态几乎每秒都会发出。

    现在是编写代码的时候了。我将代码简化为我认为能够提供足够信息的部分以及问题发生的位置:


    QWidget内部

    # ...
    
    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() 插槽被触发,代码的执行将通过 QThread.sleep(5) .我认为这也会影响启动,但我的小部件有多个实例(每个实例都控制自己的线程,其中有一个工作线程和一个计时器),所有运行启动的操作都按预期进行-按钮,用于触发 启动() 停止() 插槽,禁用5秒钟,然后启用。使用 停止() 被触发时,这种情况根本不会发生。

    我真的无法解释这种行为。更糟糕的是,我通过 Q_属性 设置器 self.setStatus(...) 由于冰冻而耽搁,这导致我的一些额外的电话 cleanup() 该函数基本上删除生成的文件。

    知道这里发生了什么吗?插槽和信号的本质是,一旦发出信号,连接到它的插槽就会立即被调用。因为UI运行在不同的线程中,所以我不明白为什么会发生这种情况。

    1 回复  |  直到 8 年前
        1
  •  0
  •   rbaleksandar    8 年前

    我实际上纠正了我问题中出现问题的地方。在我的原始代码中,我忘记了 @ pyqtSlot() 我的 stop() 作用添加后,效果非常好。我不知道这样的事情会引起这么大的问题!