鱼C论坛

 找回密码
 立即注册
查看: 3692|回复: 8

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

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

直接上代码
  1. from PyQt5.QtCore import pyqtSignal, pyqtSlot, QThread, QTimer
  2. from PyQt5.QtWidgets import QApplication, QWidget
  3. from PyQt5 import uic
  4. import sys
  5. import datetime
  6. import threading
  7. from time import sleep

  8. class PrintThread(QThread):
  9.     resSignal = pyqtSignal(str , str)

  10.     def __init__(self):
  11.         super().__init__()
  12.         self.p_timer = None
  13.         self.paused = threading.Event()
  14.         self.paused.set()

  15.     def run(self):
  16.         self.p_timer = QTimer()
  17.         self.p_timer.moveToThread(self)
  18.         self.p_timer.setInterval(1000)
  19.         self.p_timer.timeout.connect(self.query_and_print)
  20.         self.exec_()

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

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

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

  37. class Mywindow(QWidget):
  38.     pauseSignal = pyqtSignal()
  39.     resumeSignal = pyqtSignal()

  40.     def __init__(self):
  41.         super().__init__()
  42.         self.init_ui()

  43.         self.p_thread = PrintThread()
  44.         self.p_thread.resSignal.connect(self.update_to_mysql)
  45.         self.pauseSignal.connect(self.p_thread.pause)
  46.         self.resumeSignal.connect(self.p_thread.resume)
  47.         self.p_thread.start()

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

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

  58.     def update_to_mysql(self):
  59.         pass

  60. if __name__ == "__main__":
  61.     app = QApplication(sys.argv)
  62.     w = Mywindow()
  63.     w.ui.show()
  64.     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的启停比如
  1.     def run(self):
  2.         a = 0
  3.         def query_and_print():
  4.             nonlocal a
  5.             print(f'更新数据,时间{datetime.datetime.now()}')
  6.             sleep(1)
  7.             a += 1
  8.             if a == 3:
  9.                 p_timer.stop()

  10.         loop = QEventLoop()
  11.         p_timer = QTimer()
  12.         p_timer.setInterval(1000)
  13.         p_timer.timeout.connect(query_and_print)
  14.         p_timer.start()
  15.         loop.exec_()
复制代码

在这个情况下,怎么从主界面通过按钮发送信号控制子线程中QTimer的暂停与启动。
4、除了在子线程中用while循环模拟QTimer行为,还有没有别的好方法?
5、最终目的是在主线程中调用fun1()时,先暂停子线程的计时器(因为计时器调用的函数会使用另外一个app,必须确保主线程使用该app时的优先性),然后等主线程运行完毕后再由子线程接管这个app。有没有更好的解决办法?
最佳答案
2024-8-13 23:22:31
去除多余的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_()

本帖被以下淘专辑推荐:

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

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

  9. class PrintThread(QThread):
  10.     resSignal = pyqtSignal(str , str)
  11.     pauseSignal = pyqtSignal()
  12.     resumeSignal = pyqtSignal()

  13.     def __init__(self):
  14.         super().__init__()
  15.         self.paused = threading.Event()
  16.         self.paused.set()
  17.         self.a = 0

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

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

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

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

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

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

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

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

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

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

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


  62. class Mywindow(QWidget):

  63.     def __init__(self):
  64.         super().__init__()
  65.         self.init_ui()

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

  67.         self.p_thread = PrintThread()
  68.         self.p_thread.resSignal.connect(self.update_to_mysql)
  69.         self.p_thread.start()

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

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

  76.         self.p_thread.request_resume()


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

  81.     def update_to_mysql(self):
  82.         pass

  83. if __name__ == "__main__":
  84.     app = QApplication(sys.argv)
  85.     w = Mywindow()
  86.     w.ui.show()
  87.     app.exec()
复制代码
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

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


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

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-10-3 09:04

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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