何时使用事件而不是将引用传递给父对象(PyQT)



我正在努力了解以下哪种方法更好。我将使用PyQt5来说明这个示例,因为这是我正在使用的系统。

我们有一个类ControlWidget(QWidget),它提供UI组件并生成只是执行某个任务的辅助线程。如果其中一个工作程序出现故障,我们希望通知父级,以便它可以直观地更新用户并执行一些其他操作,如清除/禁用用户输入。

方法1是从遇到错误的工作者发送一个事件,并在父实例中捕获它。worker类有一个errorSignal,它只是指示错误的存在。

# The worker class
Class Worker(QObject, Thread):
errorSignal = pyqtSignal(bool)
def __init__(self):
super().__init__()
self._running = True
def run(self):
while self._running:
try:
# do some work here that might fail
except:
# notify parent that there was an error
errorSignal.emit(True)
self._running = False
def terminate(self):
self._running = False
# The parent class
class ControlWidget(QWidget):
def __init__(self):
super().__init__()

# instantiate the workers
self.worker1 = Worker()
self.worker2 = Worker()
# connect to the error signal
self.worker1.errorSignal.connect(self.handle_error)
self.worker2.errorSignal.connect(self.handle_error)
# start the workers
self.worker1.start()
self.worker2.start()
def handle_error(self, error):
if error:
# clean up worker threads
self.worker1.terminate()
self.worker1.join()
self.worker2.terminate()
self.worker2.join()
# notify user
self.setStyleSheet('background: red;')
# disable UI input etc
self.button.setEnabled(False)
...

方法2是在父级上创建一个error_status属性,并将一个引用传递给每个子级工作者,这样它就可以直接切换错误位。

# The worker class
Class Worker(QObject, Thread):
def __init__(self, parent):
super().__init__()
self._running = True
self.parent = parent
def run(self):
while self._running:
try:
# do some work here that might fail
except:
# notify parent that there was an error
self.parent.error_status = True
def terminate(self):
self._running = False
# The parent class
class ControlWidget(QWidget):
def __init__(self):
super().__init__()

# instantiate the workers
self.worker1 = Worker(parent=self)
self.worker2 = Worker(parent=self)
# no signals here :)
# start the workers
self.worker1.start()
self.worker2.start()
@property
def error_status(self):
return self._error_status
@error_status.setter
def error_status(self, value):
self._error_status = value
self.handle_error(value)
def handle_error(self, error):
if error:
# clean up worker threads
self.worker1.terminate()
self.worker1.join()
self.worker2.terminate()
self.worker2.join()
# notify user
self.setStyleSheet('background: red;')
# disable UI input etc
self.button.setEnabled(False)
...

每秒事件数限制

从功能上讲,这两种方法都会起作用,但是,我想我担心如果我的应用程序中有很多事件(我确实有),我会过载事件队列并锁定或错过一些事件。我在某个地方读到Qt的限制是每秒大约200万个事件(尽管现在我找不到参考资料)。这可能只是取决于可用内存。如果我可能达到这个极限,那么在可能的情况下尝试卸载一些事件处理不是更好吗?

方法2是处理此类问题的更好方法吗(假设我已经通过编辑解决了线程安全问题)?到处传递对父/子对象的引用确实让人感觉有点恶心。

也许这是一个六个,另一个六打?如有任何反馈,我们将不胜感激。

不能错过事件,如果不处理和/或忽略(这是两件相关但不同的事情),它们都会被处理并最终丢弃。

不过,你的两种方法都有缺陷。

信号

你不应该使用terminate()(医生明确表示它很危险,不鼓励使用)。

QThread也没有join()方法,它有wait(),也有同样的方法,但你应该不要在主线程中使用它:Qt自动调用接收器线程中连接的插槽,并且由于该信号连接到CCD_ 8(它是主线程中的对象的成员),结果是它将阻塞主线程。为了避免这种情况,wait()必须在工作线程中执行,因此您最终可以考虑两个信号的方法:首先对错误信号做出反应,然后执行一些操作,直到线程的finished()信号最终发出。

旗手

这样做只能部分起作用,因为setter是从工作线程调用的,所以wait()将从那里正确调用。不幸的是,您还试图访问同一函数中的UI,这是被禁止的,因为UI元素不是线程安全的。在最好的情况下,您会有一些图形工件,并且一些小部件不会得到正确更新;最坏的情况(更有可能,尤其是由于setStyleSheet()),程序将崩溃。


信号是与主线程通信的唯一正确和安全的方式,因为它们允许正确的事件处理,并防止在等待线程退出时冻结UI。

如果在线程中发生其他事情时需要更改UI中的某些内容,请在驻留在主线程中的对象中创建一个函数,并将其连接到将从其他线程发出的适当信号。如果在等待线程时需要执行可能会阻塞的操作,请在该线程中执行。

我在这里发布了一个我自己的答案,基于@ekhurmo和@musicamante的有用评论,以及我所做的一些进一步挖掘。

我的主要反对意见是,到目前为止的建议似乎都认为线程之间只有方式可以通过信号进行数据通信。虽然在许多/大多数情况下,这可能是最好的选择,也是一个很好的经验法则,但还有其他安全的方法可以做到这一点。我继续击败这匹可能已经死了的马的原因是,在Qt文档中,它提到了对信号数量的限制:

在i586-500上,连接到一个接收器每秒可以发射约2000000个信号,连接到两个接收器每秒可发射约1200000个信号

假设这里建议的硬件已经过时,并且当前的Qt版本文档没有这样的参考。然而,与正常回调相比,信号确实会带来额外的开销(这在过去和现在的Qt-docs版本中都有提及)。虽然在我的情况下不太可能达到这个极限,但我现在更感兴趣的是了解这两种方法的优点和缺点。

我认为这两种方法都可以以线程安全的方式使用,尽管在最初的问题中没有正确地解决这个问题。这两种方法都在这里发布了完整的MWE。

方法1:使用信号

# thread-signal.py
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QPushButton, QVBoxLayout
from threading import Thread
import time
# The worker class
class Worker(QObject, Thread):
errorSignal = pyqtSignal(bool)
def __init__(self, id=""):
super().__init__()
self.id = id
self._running = True
def run(self):
while self._running:
try:
# do some work here that might fail
time.sleep(2)
raise ValueError(f"Worker Failure")
except:
# notify parent that there was an error
print(f"Worker {self.id} failed")
self.errorSignal.emit(True)
self._running = False
def terminate(self):
self._running = False
# The parent class
class ControlWidget(QWidget):
def __init__(self):
super().__init__()

# set up UI
vbox = QVBoxLayout()
self.button = QPushButton("Test")
vbox.addWidget(self.button)
self.setLayout(vbox)

# instantiate the workers
self.worker1 = Worker(id=1)
self.worker2 = Worker(id=2)

# connect to the error signal
self.worker1.errorSignal.connect(self.handle_error)
self.worker2.errorSignal.connect(self.handle_error)

# start the workers
self.worker1.start()
self.worker2.start()

def handle_error(self, error):
if error:
# clean up worker threads
self.worker1.terminate()
self.worker2.terminate()

# notify user
self.button.setStyleSheet('background: red;')

# disable UI input etc
self.button.setEnabled(False)
print(f"Worker 1 isAlive = {self.worker1.is_alive()}")
print(f"Worker 2 isAlive = {self.worker2.is_alive()}")
if __name__=='__main__':
app = QApplication([])

window = QMainWindow()
window.show()

ctrl = ControlWidget()
window.setCentralWidget(ctrl)
# window.setLayout(layout)

app.exec()

运行该示例的结果是,在2秒钟后,Worker失败(Worker 1首先失败,因为它是首先启动的——尽管根据线程之间共享GIL的方式,情况可能并不总是如此)。最后,两个工作程序都被关闭,并且控制小部件被停用。

$ python thread-signal.py 
Worker 1 failed
Worker 1 isAlive = False
Worker 2 isAlive = True
Worker 2 failed
Worker 1 isAlive = False
Worker 2 isAlive = False

方法2:使用parent参考文献

# thread-reference.py
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QPushButton, QVBoxLayout
from threading import Thread, Lock
import time
# global thread lock
lock = Lock()
# The worker class
class Worker(Thread):
def __init__(self, id="", parent=None):
super().__init__()
self.id = id
self.parent = parent
self._running = True
def run(self):
while self._running:
try:
# do some work here that might fail
time.sleep(2)
raise ValueError(f"Worker Failure")
except:
with lock:
# notify parent that there was an error
print(f"Worker {self.id} failed")
self._running = False
self.parent.error_status = True
def terminate(self):
self._running = False
# The parent class
class ControlWidget(QWidget):
def __init__(self):
super().__init__()

# set up UI
vbox = QVBoxLayout()
self.button = QPushButton("Test")
vbox.addWidget(self.button)
self.setLayout(vbox)

# instantiate the workers
self.worker1 = Worker(id=1, parent=self)
self.worker2 = Worker(id=2, parent=self)

# start the workers
self.worker1.start()
self.worker2.start()
def handle_error(self, error):
if error:
# clean up worker threads
self.worker1.terminate()
self.worker2.terminate()

# notify user
self.button.setStyleSheet('background: red;')

# disable UI input etc
self.button.setEnabled(False)
print(f"Worker 1 isAlive = {self.worker1.is_alive()}")
print(f"Worker 2 isAlive = {self.worker2.is_alive()}")
@property
def error_status(self):
return self._error_status

@error_status.setter
def error_status(self, value):
self._error_status = value
self.handle_error(value)
if __name__=='__main__':
app = QApplication([])

window = QMainWindow()
window.show()

ctrl = ControlWidget()
window.setCentralWidget(ctrl)

app.exec()

在这种情况下,最终结果是相似的,工人被关闭,控制小部件被停用,但对输出的检查显示了一个有趣的差异:

$ python thread-reference.py 
Worker 1 failed
Worker 1 isAlive = True
Worker 2 isAlive = True
Worker 2 failed
Worker 1 isAlive = False
Worker 2 isAlive = True

直到handle_error方法完成之后,线程的isAlive()值才解析为False。我猜这只是由于GIL导致字节码执行的方式。

最新更新