V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
wangyz1997
V2EX  ›  Python

求助: PyQt5 的一个线程占用 CPU 导致另一个线程响应变慢

  •  
  •   wangyz1997 · 258 天前 · 2564 次点击
    这是一个创建于 258 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我之前是搞嵌入式的,现在要写一个上位机,选择使用 Python+PyQt5 来完成。我的程序有两个执行线程以及一个主线程,主线程初始化完两个执行线程之后,一个执行线程进行串口的数据交互,另一个线程执行一个比较耗时的操作( OpenCV 图像处理),完全占满 CPU 。 这两个线程都是使用 QThread 来完成的,现在遇到了一个问题:图像处理线程长时间的计算,会导致串口线程出现响应变慢的现象(串口丢包导致通信失败),与此同时主线程的 UI 操作也会卡顿。 下面是部分代码。

    class MainWindowClass(QMainWindow, Ui_MainWindow):
    	def __init__(self):
    		super(MainWindowClass, self).__init__()  # 初始化父类
    		self.setupUi(self)  # 初始化窗口
    
    		self.imgProcTrd = ThreadImageProc(op_param, op_cam_idx)  # 创建图像处理线程
    		self.imgProcTrd.signalImageSend.connect(self.callback_image_display)  # 连接回传到 GUI 的事件
    
    		self.serialCommTrd = ThreadSerialComm()  # 创建串口通信线程
    		self.serialCommTrd.signalSerialStatus.connect(self.callback_serial_event)
    
    
    class ThreadImageProc(QThread):
    	#  通过类成员对象定义信号对象
    	signalImageSend = pyqtSignal(numpy.ndarray)
    
    	def __init__(self, op_param, cam_idx):
    		super(ThreadImageProc, self).__init__()
    
    	def run(self):
    		while True:
    			ret, cap_img = cap.read()
    			img = self.image_proc(cap_img)
    			self.signalImageSend.emit(img)
    
    
    class ThreadSerialComm(QThread):
    	signalSerialStatus = pyqtSignal(list)  # 串口上报 
    
    	def __init__(self):
    		super(ThreadSerialComm, self).__init__()
    
    	def run(self): 
    		while True:
    		# 省略串口通信函数
    

    想请问一下各位大佬这是什么情况?有没有解决方案? RTOS 是抢占式系统,即使是单核心的嵌入式处理器也会保证每过一个 Tick 执行一次任务调度来确保高优先级的任务得以抢占 CPU 。按照我的理解,电脑这种多核心处理器应该是把多个线程分配给多个核心执行的(仅仅是我很初级的理解,望赐教),为什么会出现这种子线程卡死其他线程甚至 UI 线程的呢?有没有什么解决方法?

    29 条回复    2020-05-12 09:59:20 +08:00
    gainsurier
        1
    gainsurier   258 天前
    用一些 perf 或者 monitor 软件看一下 cpu 占用的热点函数,然后针对优化
    zhuangzhuang1988
        2
    zhuangzhuang1988   258 天前
    如果可以的话换 C# + Emgu CV 试试
    wangyz1997
        3
    wangyz1997   258 天前
    @gainsurier 我已经知道是哪个函数占用时间了(因为删掉这个函数速度就正常了),但是我好奇的是为什么我明明把耗时的函数从主线程里分离出来了,还是会导致主线程甚至其他线程响应慢。

    @zhuangzhuang1988 已经开发了很多了,应该是没法换了……而且我是搞嵌入式的半路出家的,让我在学一门语言难度还是偏高。
    jin7
        4
    jin7   258 天前   ❤️ 1
    python 的多线程好像是假的?
    肯定有办法解决 不然别人怎么能行
    weyou
        5
    weyou   258 天前 via Android   ❤️ 2
    CPU 密集型操作线程受到 GIL 的限制比较大,几乎相当于单线程。可以使用 multiprocessing 避免这个问题
    aa6563679
        6
    aa6563679   258 天前 via iPhone   ❤️ 1
    用多进程吧,顺便把子进程 CPU 优先级降低
    wangyz1997
        7
    wangyz1997   258 天前
    @weyou
    @aa6563679
    感谢,我去试一试
    wangyz1997
        8
    wangyz1997   258 天前
    @weyou 如果我调用的是 C 语言封装好的库,还受解释器锁的限制吗?
    weyou
        9
    weyou   258 天前 via Android
    @wangyz1997 这是由这个库决定的。比如 Qt 就会在 C++的 boundary 处释放 GIL,但是 QThread 的线程体是 Python code 的话,同样会受到 GIL 的控制,不能例外
    zk8802
        10
    zk8802   258 天前 via iPhone   ❤️ 1
    如果耗时的函数的运行时间不是很关键,你可以在 while True 循环里面加上 time.sleep(0.0000001) 以定期释放 GIL 。具体多少个循环调用一次 sleep,需要调试一下、看看如何才能不卡。
    inframe
        11
    inframe   258 天前 via Android   ❤️ 1
    建议:
    用 process explorer 看看 CPU 密集运行时,
    工作线程的状态,检查一下代码是否运行在 Gil 限制中,可以 suspend 一个进程,然后挨个查看线程堆栈

    部分代码用 Cython 重写一下编译为 pyd 加载,
    只要不是 Python native code 都不会遇到 Gil 限制多核心,当然 multi process 也可以
    Drahcir
        12
    Drahcir   258 天前   ❤️ 1
    由于 GIL,Python CPU 密集型任务不要用多线程,用 multiprocessing 。自己做好进程间通信即可。
    虽然有 C-extension 库(比如 pyqt )可以在 C 空间内突破 GIL,但是只要存在 python / c 交互,就仍受 GIL 限制。
    imn1
        13
    imn1   258 天前   ❤️ 1
    两个 Thread 都是 while True,也没有看到结束条件,无限执行?建议改为有条件循环

    ThreadSerialComm 是跟随主线程不断执行的么?关闭窗口才结束?
    如果是这样,建议改为定时触发,QtCore.QTimer(),while 用队列判断,有通信请求扔进队列,判断队列不是 empty 才执行线程,empty 就结束,等待下次 timer 触发

    全部都是 while true 、又没有结束条件应该是症结所在
    wangyz1997
        14
    wangyz1997   258 天前
    @zk8802
    @inframe
    @Drahcir
    感谢,看来要学习一下多进程了
    wangyz1997
        15
    wangyz1997   258 天前
    @imn1 我在 ThreadSerialComm 里有一个超时 50ms 的 IO 操作(串口通信),是不是这样就和 QTimer 的效果差不多了呢?
    imn1
        16
    imn1   257 天前
    @wangyz1997 #15
    我觉得不行,我说不清楚,我也不熟悉线程
    不过可以说说我写的例子
    一个消息 thread,发送右下角消息,因为不论那个控件发出,都由这个 thread 管理,所以设后台 timer 循环每两秒触发
    一个 hash thread,多文件 hash,也是长时间,有时长达几十分钟甚至一小时,由按钮触发,循环使用 for
    当然还有其他(共 10+个 thread class ),不过 hash 时很吃 CPU,一般也不作其他复杂操作,但问题不在这里

    如果我把 timer 放到 thread class 里面,就是 start 后,根据 timer 2 秒一次循环,单单这样,主窗口就已经反应迟缓了,所以根本不是 hash 的问题,因为都没启动。后来改成 timer 放在主线程,init 时启动 timer,每次 timeout 时触发 thread 检查消息队列,这样就没问题了
    另外,建议长时间的 thread,在每次循环都 emit 一次(我是 emit 到进度条),这样也相当于打个“断点”,对 py 处理线程有帮助

    上述这些我都说不出什么道理,反正看看别人的例子,然后想想协程管理那种也是这样打“断点”切换,自己摸索着理顺的
    jones2000
        17
    jones2000   257 天前
    线程里用信号等待来触发, 不要 while(true) 这样你的线程还是再占用 cpu,需要用 WaitForSingleObject 这些函数, 才能释放当前线程不使用 cpu, 等到信号。
    wangyz1997
        18
    wangyz1997   257 天前
    @imn1 感谢
    wangyz1997
        19
    wangyz1997   257 天前
    @jones2000 我的图像处理线程是读取摄像头,然后进行图像算法并将结果 emit 到主线程中。我想法中是让这个线程以最高的速度运行,不需要接受外部的信号之类的,只需要完成它自己的读取-处理-发送就可以了。请问这样该怎么释放 CPU 呢?
    imn1
        20
    imn1   257 天前
    @wangyz1997 #19
    我比较好奇是你每一帧都要捕捉么?不需要长期运行捕捉 thread 的吧

    我觉得你这个是有顺序执行的,没必要分两个 thread,写到一个 thread 里面两个函数顺序执行就好
    如果是多次捕捉并处理,应该在 thread 内用多进程并行

    另外一个 thread 不应多次运行,所以我基本都有类似的语句先判断才启动
    if not self.hashThread.isRunning():self.hashThread.start()
    jones2000
        21
    jones2000   257 天前
    @wangyz1997 关键点在你的读取的这个地方, 如果你的数据是有更新频率的,那读取一次完成以后, 需要释放 cpu (使用信号等待几微妙), 在下次频率更新以后在读取。
    你现在的逻辑就是一个死循环一直读, 这样就肯定是不会释放 cpu, 如果你是这样模式的,那就使用独占一个 cpu 的方式, 比如你是 16 核的服务器,你就单独拿出 1-2 个核单独计算你的图形数据。 或者单独拿一个服务器计算图形,  UI 显示用其他的机器显示, 通过 tcp 推送的方式,直接把计算好的数据发到前端, 这样前端是绘图肯定不卡。
    wangyz1997
        22
    wangyz1997   257 天前
    @jones2000 是的,我准备学习一下多进程。因为图像处理本来就很慢,希望它能够以最高的速度运行,所以是死循环读。
    wangyz1997
        23
    wangyz1997   257 天前
    @imn1 是每一帧都要捕捉。因为图像算法本来就比较慢(个位数 fps ),因此希望能够充分发挥计算能力。因为串口通信有一个超时问题,所以这两个只能分开来做了。感谢你的回复。
    nightwitch
        24
    nightwitch   257 天前   ❤️ 1
    Python 由于 GIL 的存在,多线程是假的,实质上只有一个线程在运行,只是解释器在切换任务。opencv 的部分新起一个进程,数据传过去,计算完了以后拿回来。
    youngce
        25
    youngce   257 天前   ❤️ 1
    @nightwitch #24 GIL 保证解释器同时只解释一个 py 线程的代码, 但是如果这个线程 IO 阻塞了, 解释器就跳到其它线程。 多线程是真的, 只不过由于 GIL 的存在,导致了 Cpython 中无法利用多线程实现多核处理器并发处理 CPU 密集操作。所以说到底,python 的 io 阻塞可以利用协程、多线程实现异步,而 cpu 密集的操作只能依赖于多进程。
    defphilip
        26
    defphilip   257 天前   ❤️ 1
    把核心的计算程序,串口读写全部封装到 C++代码上,包括开线程,然后通过事件通知的方式回调告诉主线程,这样你就可以在同一个进程空间内干这两件事情,并且你还能享受到 python 的部分遍历

    另外既然都用 Qt 了,为何不选择直接用 C++ Qt 完成呢?
    weyou
        27
    weyou   257 天前 via Android   ❤️ 1
    @nightwitch 你没有 get 到任何一点啊,Python 的多线程是真的,而且这里是 Qt 的多线程,也是真的。线程切换自然也是操作系统管理的,而不是 Python 解释器。只是 CPU 密集型操作下因为 GIL 的存在,导致每个线程都需要获取到 GIL 才能运行,基本“等价于”线性执行的,但不是你理解的只有一个线程存在。
    enrio
        28
    enrio   257 天前   ❤️ 1
    CPU 密集型+Python+多线程=>多进程
    wangyz1997
        29
    wangyz1997   257 天前
    @defphilip 有一些库只有 Python 有,若是重做轮子太费时费力了。
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2585 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 71ms · UTC 04:00 · PVG 12:00 · LAX 20:00 · JFK 23:00
    ♥ Do have faith in what you're doing.