我正在使用 PySide6 实现一个小型多线程 GUI 应用程序,以从(USB 连接的)传感器获取数据并使用 Qt 可视化数据。即,用户可以启动和停止数据获取:
单击播放按钮时,将创建一个工作对象并将其移动到QThread
,然后QThread
启动。互联网上有很多不同的方法来实现这种无限期(数据获取循环)。以下是我迄今为止最常遇到的两种主要方法:
- 方法 1(无限但用户可中断的循环加
sleep()
):
class Worker(QObject):
def __init__(self, user_stop_event: Event)
super().__init__()
self.user_stop_event = user_stop_event
def run(self):
while not self.user_stop_event.isSet():
self.fetch_data()
# Process received signals:
Qtcore.QApplication.processEvents()
# A sleep is important since in Python threads cannot run really in parallel (on multicore systems) and without sleep we would block the main (GUI) thread!
QThread.sleep(50)
def fetch_data(self):
...
- 方法 2(基于计时器的方法):
class Worker(QObject):
def __init__(self):
super().__init__()
timer = QTimer()
timer.timeout.connect(self.fetch_data)
timer.start(50)
def fetch_data(self):
...
两种方法都使用相同的机制来启动线程:
thread = QThread()
worker = Worker()
worker.moveToThread(thread )
thread.started.connect(worker.run)
...
这两种方法的优缺点是什么?哪种实现方式更可取?
不幸的是,Qt 的官方线程基础网站没有在这里给出明确的建议。 对我来说,两者都有效,但我不太确定哪一个应该作为我们后续项目的默认实现。
Qt 对此确实没有发言权,这取决于您用于通信的 API 的通信模式,以及它是否支持同步或异步 IO。
同步(阻塞)API
如果通信是同步的,例如pyserial或QSerialPort(阻塞模式),则
Approach1
无需睡眠即可使用,但在端口上添加超时。从同步 IO 读取通常会丢弃 GIL,因此您不需要睡眠。(这两个 API 会丢弃 GIL,请查看任何其他 API 的文档),Qt 文档阻塞接收器提供了一个类似的例子异步(非阻塞)API
如果你正在从异步 IO 读取数据,比如QTcpSocket或QSerialPort(异步模式),那么你可以这样做,
Approach2
因为 Qt 的事件循环在不执行 Python 代码时会丢弃 GIL,并且你可以在传输过程中中断该过程,而这在同步 IO 中是不可能的,你不需要异步 IO 的计时器直接调用
sleep
几乎总是错误的,它要么延迟数据读取,要么延迟终止信号,你的线程通常要么被同步 IO 阻塞,要么被等待异步 IO 完成的 QT 事件循环阻塞,或者任何其他异步 IO 循环asyncio
。由于同样的原因,在计时器上轮询套接字同样糟糕。如果您确实需要
sleep
(也许您只是每隔几毫秒轮询一次传感器),那么请调用QEventLoop.processEvents并传递一个deadline
带有“处理事件”时间的参数,这可确保信号可以中断您的“睡眠”(通过loop.exit()
在连接的插槽内调用),并确保在完成“处理事件”后检查此中断是否发生。在您等待时,GIL 将被丢弃,因此它就像一个可中断的睡眠。定期事件
最后,对于需要无漂移发生的周期性事件(例如,发送通知或每 50 毫秒轮询一次传感器),您应该使用
Approach2
计时器,因为 QTimer 使用 OS 计时器来避免漂移。(它在某些平台上仍会漂移,但漂移速度非常慢)这确保函数在被调用
50,100,150,200ms ... etc
,而带有睡眠的正常循环将在52,105,160,223ms... etc