PyQT - QThread.sleep(...) 在单独的线程块中 UI
PyQT - QThread.sleep(...) in separate thread blocks UI
首先,在深入研究问题和代码之前,我将简要介绍用户界面及其功能。提前抱歉,但我无法提供完整的代码(即使我可以......很多行:D)。
UI 的描述及其作用
我有一个自定义 QWidget
或更准确地说,该自定义小部件的 N 个实例在网格布局中对齐。小部件的每个实例都有自己的 QThread
,其中包含一个工人 QObject
和一个 QTimer
。就 UI 组件而言,小部件包含两个重要组件 - 可视化状态的 QLabel
和启动的 QPushButton
(通过触发 Worker 中的 start()
槽) 或停止(通过触发 worker 中的 slot()
插槽)外部进程。两个插槽都包含 5 秒的延迟,并且在执行期间还禁用按钮。 worker 本身不仅控制外部进程(通过调用上面提到的两个插槽),而且还通过 status()
插槽检查进程是否 运行,这是由 QTimer
触发的每 1 秒。如前所述,worker 和 timer 都存在于线程中! (我通过打印 main 的线程 ID(UI 所在的位置)和每个 worker 的线程 ID(与 main 100% 肯定不同)进行了双重检查。
为了减少从 UI 到 worker 的调用量,反之亦然,我决定声明 _status
属性(它保存外部进程的状态 - inactive, 运行, error) 我的 Worker
class 作为Q_PROPERTY
与 setter
、getter
和 notify
最后一个是从 setter
IF 内触发的信号,并且仅当值与旧值发生变化时.我之前的设计更加 signal/slot 密集,因为状态实际上是每秒发出一次。
现在是编写代码的时候了。我只将代码缩减到我认为可以提供足够信息的部分以及出现问题的位置:
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)
现在关于我的问题: 我注意到 UI 只有在 stop()
槽被触发并且代码执行时才会冻结通过QThread.sleep(5)
。我认为这也应该影响开始,但是我的小部件有多个实例(每个实例都控制自己的线程,其中有一个工作程序和计时器)并且所有这些 运行 开始按预期工作 - 按钮,它用于触发 start()
和 stop()
插槽,禁用 5 秒然后启用。随着 stop()
被触发,这根本不会发生。
我真的无法解释这种行为。更糟糕的是,我通过 Q_PROPERTY
setter self.setStatus(...)
发出的状态更新由于这种冻结而延迟,这导致我的 cleanup()
函数的一些额外调用基本上删除生成的文件。
知道这里发生了什么吗?槽和信号的本质是,一旦信号发出,连接到它的槽就会立即被调用。由于 UI 运行在与工作线程不同的线程中,我不明白为什么会发生这一切。
我实际上更正了问题出处。在我的原始代码中,我忘记了 stop()
函数的 pyqtSlot()
之前的 @
。添加后,它工作得很好。我不知道这样的事情会导致这么大的问题!
首先,在深入研究问题和代码之前,我将简要介绍用户界面及其功能。提前抱歉,但我无法提供完整的代码(即使我可以......很多行:D)。
UI 的描述及其作用
我有一个自定义 QWidget
或更准确地说,该自定义小部件的 N 个实例在网格布局中对齐。小部件的每个实例都有自己的 QThread
,其中包含一个工人 QObject
和一个 QTimer
。就 UI 组件而言,小部件包含两个重要组件 - 可视化状态的 QLabel
和启动的 QPushButton
(通过触发 Worker 中的 start()
槽) 或停止(通过触发 worker 中的 slot()
插槽)外部进程。两个插槽都包含 5 秒的延迟,并且在执行期间还禁用按钮。 worker 本身不仅控制外部进程(通过调用上面提到的两个插槽),而且还通过 status()
插槽检查进程是否 运行,这是由 QTimer
触发的每 1 秒。如前所述,worker 和 timer 都存在于线程中! (我通过打印 main 的线程 ID(UI 所在的位置)和每个 worker 的线程 ID(与 main 100% 肯定不同)进行了双重检查。
为了减少从 UI 到 worker 的调用量,反之亦然,我决定声明 _status
属性(它保存外部进程的状态 - inactive, 运行, error) 我的 Worker
class 作为Q_PROPERTY
与 setter
、getter
和 notify
最后一个是从 setter
IF 内触发的信号,并且仅当值与旧值发生变化时.我之前的设计更加 signal/slot 密集,因为状态实际上是每秒发出一次。
现在是编写代码的时候了。我只将代码缩减到我认为可以提供足够信息的部分以及出现问题的位置:
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)
现在关于我的问题: 我注意到 UI 只有在 stop()
槽被触发并且代码执行时才会冻结通过QThread.sleep(5)
。我认为这也应该影响开始,但是我的小部件有多个实例(每个实例都控制自己的线程,其中有一个工作程序和计时器)并且所有这些 运行 开始按预期工作 - 按钮,它用于触发 start()
和 stop()
插槽,禁用 5 秒然后启用。随着 stop()
被触发,这根本不会发生。
我真的无法解释这种行为。更糟糕的是,我通过 Q_PROPERTY
setter self.setStatus(...)
发出的状态更新由于这种冻结而延迟,这导致我的 cleanup()
函数的一些额外调用基本上删除生成的文件。
知道这里发生了什么吗?槽和信号的本质是,一旦信号发出,连接到它的槽就会立即被调用。由于 UI 运行在与工作线程不同的线程中,我不明白为什么会发生这一切。
我实际上更正了问题出处。在我的原始代码中,我忘记了 stop()
函数的 pyqtSlot()
之前的 @
。添加后,它工作得很好。我不知道这样的事情会导致这么大的问题!