首先,在深入研究问题和代码之前,我将简要描述用户界面及其功能。很抱歉,我无法提供完整的代码(即使我能提供,也有很多行: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运行在不同的线程中,所以我不明白为什么会发生这种情况。