鱼C论坛

 找回密码
 立即注册
查看: 568|回复: 7

请教pyqt5子线程内使用QTimer循环运行长耗时函数的问题

[复制链接]
发表于 2024-8-13 23:22:30 | 显示全部楼层 |阅读模式
50鱼币
本帖最后由 nsaizl 于 2024-8-13 23:29 编辑

直接上代码
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5 import uic
import sys
import datetime
import threading
from time import sleep

class PrintThread(QThread):
    resSignal = pyqtSignal(str , str)

    def __init__(self):
        super().__init__()
        self.p_timer = None
        self.paused = threading.Event()
        self.paused.set()

    def run(self):
        self.p_timer = QTimer()
        self.p_timer.moveToThread(self)
        self.p_timer.setInterval(1000)
        self.p_timer.timeout.connect(self.query_and_print)
        self.exec_()

    @pyqtSlot()
    def query_and_print(self):
        print(f'更新数据,时间{datetime.datetime.now()}')

    @pyqtSlot()
    def pause(self):
        self.paused.clear()
        if self.p_timer.isActive():
            self.p_timer.stop()  # 停止定时器
        print("定时器已暂停")

    @pyqtSlot()
    def resume(self):
        print('恢复定时器')
        if not self.p_timer.isActive():
            self.p_timer.start(1000)  # 重新启动定时器
        self.paused.set()
        print("定时器已恢复")

class Mywindow(QWidget):
    pauseSignal = pyqtSignal()
    resumeSignal = pyqtSignal()

    def __init__(self):
        super().__init__()
        self.init_ui()

        self.p_thread = PrintThread()
        self.p_thread.resSignal.connect(self.update_to_mysql)
        self.pauseSignal.connect(self.p_thread.pause)
        self.resumeSignal.connect(self.p_thread.resume)
        self.p_thread.start()

    def fun1(self):
        self.pauseSignal.emit()  # 发送暂停信号
        for i in range(3):
            sleep(1)
            print(f'正在运行{i}')
        self.resumeSignal.emit()  # 发送恢复信号

    def init_ui(self):
        self.ui = uic.loadUi("./测试.ui") #这里面只有一个按钮
        self.btn1 = self.ui.pushButton
        self.btn1.clicked.connect(self.fun1)

    def update_to_mysql(self):
        pass

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Mywindow()
    w.ui.show()
    app.exec() 

有以下几个问题:
1、self.p_timer = QTimer() 是在子线程中创建的,但是由子线程的中调用pause()函数调用self.p_timer.stop()时会报错,提示不能从其他线程停止QTimer。使用threading.get_ident()后发现pause()函数运行时的确与子线程的ID不同,与主线程是一致的。请问是什么原因?
2、如果将self.p_timer = QTimer() 放到子线程的init()中,那么主线程创建实例时就生成了计时器,所用的线程还是主线程,没有达到多线程运行的目的。对于长时间的函数query_and_print()依然会阻塞主界面。如果一定要这么做,有什么解决的办法?或者说明肯定不能放在init里面的原因。
3、子线程PrintThread中的run函数,如果创建了p_timer = QTimer() ,不使用self,直接connect一个run函数里面的子函数,是可以在子线程中运行的,也可以控制QTimer的启停比如
    def run(self):
        a = 0
        def query_and_print():
            nonlocal a
            print(f'更新数据,时间{datetime.datetime.now()}')
            sleep(1)
            a += 1
            if a == 3:
                p_timer.stop()

        loop = QEventLoop()
        p_timer = QTimer()
        p_timer.setInterval(1000)
        p_timer.timeout.connect(query_and_print)
        p_timer.start()
        loop.exec_()
在这个情况下,怎么从主界面通过按钮发送信号控制子线程中QTimer的暂停与启动。
4、除了在子线程中用while循环模拟QTimer行为,还有没有别的好方法?
5、最终目的是在主线程中调用fun1()时,先暂停子线程的计时器(因为计时器调用的函数会使用另外一个app,必须确保主线程使用该app时的优先性),然后等主线程运行完毕后再由子线程接管这个app。有没有更好的解决办法?

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-8-14 10:40:28 | 显示全部楼层
1. QTimer的线程安全问题
在PyQt中,QTimer对象应当与其关联的QObject(如QWidget)在同一线程中创建和使用。由于信号和槽的连接可能跨越线程边界,导致行为不确定或错误。QTimer应当始终在主线程(GUI线程)中创建和管理,即使它的定时事件需要在其他线程中处理。
解决方案
将QTimer放在主线程中,通过信号和槽将定时事件的处理转移到子线程。
2. QTimer在__init__中创建的问题
在__init__中创建QTimer会使其与创建它的线程(通常是主线程)绑定。应当在子线程的run方法中创建并启动QTimer,但正如之前所述,更好的做法是将QTimer放在主线程中。
3. 控制子线程中QTimer的启停
可以通过信号和槽来控制子线程中的任务执行。在子线程中,您可以使用QEventLoop来等待事件,但请注意,QEventLoop应仅在需要时才在子线程中创建,并且通常只应有一个实例。
修改
class PrintThread(QThread):  
    # 移除QTimer相关的属性和方法  
    # ...  
  
    def run(self):  
        # 创建一个工作队列或事件循环  
        self.worker_loop = True  
  
        while self.worker_loop:  
            # 模拟长时间任务  
            sleep(1)  
            print(f'更新数据,时间{datetime.datetime.now()}')  
  
            # 检查是否需要暂停  
            if self.paused.is_set():  
                # 暂停  
                while self.paused.is_set():  
                    sleep(0.1)  # 简短休眠,避免忙等  
  
    # 暂停和恢复方法  
    def pause(self):  
        self.paused.set()  
  
    def resume(self):  
        self.paused.clear()  
  
# 在主窗口类中  
class Mywindow(QWidget):  
    # ...  
    def fun1(self):  
        self.p_thread.pause()  # 发送暂停信号  
        # ...  
        self.p_thread.resume()  # 发送恢复信号  
  
# 主程序保持不变
4. 替代QTimer的方法
使用QTimer与信号槽系统
使用Python标准库中的threading.Timer,但需注意GUI线程的安全
使用asyncio和QEventLoop的集成
5. 优先处理主线程任务
在场景中,主线程执行的任务应当具有高优先级。使用信号和槽来管理子线程的任务执行是一个很好的方式。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2024-8-14 14:09:34 | 显示全部楼层
目前找到一个方法可以实现,但是代码很不好看。有人可以优化一下吗?
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QThread, QTimer, QEventLoop, QMetaObject, Qt, Q_ARG
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5 import uic
import sys
import datetime
import threading
from time import sleep
import os

class PrintThread(QThread):
    resSignal = pyqtSignal(str , str)
    pauseSignal = pyqtSignal()
    resumeSignal = pyqtSignal()

    def __init__(self):
        super().__init__()
        self.paused = threading.Event()
        self.paused.set()
        self.a = 0

    def run(self):
        def query_and_print():
            print(f"#query_and_print线程ID: {threading.get_ident()},时间{datetime.datetime.now()}")
            if not self.paused.is_set():
                return
            sleep(1)

        def pause():
            if self.p_timer and self.p_timer.isActive():
                print(f"暂停时的ID: {threading.get_ident()}")
                self.p_timer.stop()  # 停止定时器
                print("定时器已暂停")  

        def resume():
            if not self.p_timer.isActive():
                print(f"恢复时的ID: {threading.get_ident()}")
                self.p_timer.start()  # 重新启动定时器
            self.paused.set()
            print("定时器已恢复")  

        loop = QEventLoop()
        # 连接pauseSignal到pause函数
        self.pauseSignal.connect(pause)
        self.resumeSignal.connect(resume)

        print(f"子线程ID: {threading.get_ident()}")
        self.p_timer = QTimer()
        self.p_timer.setInterval(1000)
        self.p_timer.timeout.connect(query_and_print)

        #只启动一次
        if self.a == 0:
            self.p_timer.start()
            self.a = 1

        loop.exec_()  # 启动事件循环

    @pyqtSlot()
    def emit_pause_signal(self):
        # 在子线程中发射暂停信号
        self.pauseSignal.emit()

    def request_pause(self):
        # 从主线程调用这个方法来请求暂停
        QMetaObject.invokeMethod(self, "emit_pause_signal", Qt.DirectConnection)

    @pyqtSlot()
    def emit_resume_signal(self):
        # 在子线程中发射暂停信号
        self.resumeSignal.emit()

    def request_resume(self):
        # 从主线程调用这个方法来请求运行
        QMetaObject.invokeMethod(self, "emit_resume_signal", Qt.DirectConnection)


class Mywindow(QWidget):

    def __init__(self):
        super().__init__()
        self.init_ui()

        print(f"主线程ID: {threading.get_ident()}")

        self.p_thread = PrintThread()
        self.p_thread.resSignal.connect(self.update_to_mysql)
        self.p_thread.start()

    def fun1(self):
        self.p_thread.request_pause()

        print(f"发送暂停信号的线程ID: {threading.get_ident()}")
        for i in range(10):
            sleep(1)
            print(f'正在运行{i}')

        self.p_thread.request_resume()


    def init_ui(self):
        self.ui = uic.loadUi("./测试.ui")
        self.btn1 = self.ui.pushButton
        self.btn1.clicked.connect(self.fun1)

    def update_to_mysql(self):
        pass

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Mywindow()
    w.ui.show()
    app.exec()
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-8-15 14:43:56 | 显示全部楼层
去除多余的QEventLoop和QTimer管理:直接使用QThread的run方法循环,并通过条件变量来控制暂停和恢复。
简化信号和槽的使用:直接在QThread中使用pyqtSignal来控制状态,无需额外的emit_函数。
移除不必要的threading.Event:QThread的isRunning方法已经足够用来判断线程是否在运行,结合条件变量(虽然在这个简单示例中未直接使用,但可以通过QMutex和QWaitCondition来实现更复杂的同步)。
以下是优化后的代码:
from PyQt5.QtCore import pyqtSignal, QThread, Qt  
from PyQt5.QtWidgets import QApplication, QWidget  
from PyQt5 import uic  
import sys  
import time  
  
class PrintThread(QThread):  
    pauseSignal = pyqtSignal()  
    resumeSignal = pyqtSignal()  
  
    def __init__(self):  
        super().__init__()  
        self.paused = False  
  
    def run(self):  
        while True:  
            if self.paused:  
                time.sleep(0.1)  # 简单轮询检查暂停状态  
                continue  
              
            print(f"打印: {time.ctime()}")  
            time.sleep(1)  # 模拟查询和打印操作  
  
    def pause(self):  
        self.paused = True  
        self.pauseSignal.emit()  
  
    def resume(self):  
        self.paused = False  
        self.resumeSignal.emit()  
  
class MyWindow(QWidget):  
    def __init__(self):  
        super().__init__()  
        self.init_ui()  
  
        self.p_thread = PrintThread()  
        self.p_thread.start()  
  
    def fun1(self):  
        self.p_thread.pause()  
  
        print(f"发送暂停信号: {time.ctime()}")  
        for i in range(10):  
            time.sleep(1)  
            print(f'主线程正在运行: {i}')  
  
        self.p_thread.resume()  
  
    def init_ui(self):  
        self.ui = uic.loadUi("./测试.ui")  
        self.btn1 = self.ui.pushButton  
        self.btn1.clicked.connect(self.fun1)  
  
if __name__ == "__main__":  
    app = QApplication(sys.argv)  
    w = MyWindow()  
    w.ui.show()  
    app.exec_()
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2024-8-16 09:31:10 | 显示全部楼层
很cool的阳 发表于 2024-8-15 14:43
去除多余的QEventLoop和QTimer管理:直接使用QThread的run方法循环,并通过条件变量来控制暂停和恢复。
简 ...


为了偷懒,目前也是这样写的。在run方法中加个循环。
但是还是想讨论验证一下QTimer在子线程中运行的逻辑。这玩意设计的初衷是什么?为什么在子线程中这么不好用。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-8-21 12:04:56 | 显示全部楼层
nsaizl 发表于 2024-8-14 14:09
目前找到一个方法可以实现,但是代码很不好看。有人可以优化一下吗?

这代码挺好的,很完美,没有说“难看”吧
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-8-21 14:09:54 | 显示全部楼层
刚刚学Python不久,感觉这程序看着很长诶,拆分学习学习,里面提到了类
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2024-9-2 17:17:37 | 显示全部楼层
nsaizl 发表于 2024-8-16 09:31
为了偷懒,目前也是这样写的。在run方法中加个循环。
但是还是想讨论验证一下QTimer在子线程中运行的 ...

QTimer 是 Qt 提供的一个定时器类,它允许你在指定的时间间隔后执行某些操作。QTimer 的设计初衷是为了在 GUI 应用程序中实现时间相关的功能,比如动画、定时更新等,而不会阻塞主线程的用户界面。

在主线程(通常是 GUI 线程)中使用 QTimer 是非常简单和直接的,因为它自动与主线程的事件循环集成。然而,在子线程中使用 QTimer 就比较复杂,因为每个 QObject(包括 QTimer)都有一个与之关联的线程亲和性(thread affinity),这意味着 QObject 必须在它被创建的线程中执行它的事件和信号槽。因此,你不能在一个线程中创建 QTimer 对象,然后期望它在另一个线程中启动和运行。

在子线程中使用 QTimer 的正确方式是确保 QTimer 对象在该子线程中创建,并且该子线程有一个正在运行的事件循环。这通常是通过继承 QThread 类并重写 run() 方法来实现的,然后在 run() 方法内部创建 QTimer 对象,并调用 QThread::exec() 来启动事件循环。例如:

```cpp
class WorkerThread : public QThread
{
    Q_OBJECT
public:
    WorkerThread() {}
    void run() override {
        QTimer timer;
        timer.setInterval(1000);
        connect(&timer, &QTimer::timeout, this, &WorkerThread::onTimeout);
        timer.start();
        exec(); // 启动事件循环
    }

signals:
    void timeout();
};

void WorkerThread::onTimeout() {
    // 处理定时器超时
    qDebug() << "Timeout in thread:" << QThread::currentThreadId();
}
```

`WorkerThread` 继承自 `QThread`,在它的 `run` 方法中创建了 `QTimer` 对象,并启动了事件循环。这样,定时器就可以在子线程中正常工作了。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-12-22 16:49

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表