求助: PyQt5 的一个线程占用 CPU 导致另一个线程响应变慢 - V2EX
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 2020-05-10 22:24:49 +08:00 4994 次点击
    这是一个创建于 1986 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我之前是搞嵌入式的,现在要写一个上位机,选择使用 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  
       2020-05-10 22:31:55 +08:00
    用一些 perf 或者 monitor 软件看一下 cpu 占用的热点函数,然后针对优化
    zhuangzhuang1988
        2
    zhuangzhuang1988  
       2020-05-10 22:32:11 +08:00
    如果可以的话换 C# + Emgu CV 试试
    wangyz1997
        3
    wangyz1997  
    OP
       2020-05-10 22:39:49 +08:00
    @gainsurier 我已经知道是哪个函数占用时间了(因为删掉这个函数速度就正常了),但是我好奇的是为什么我明明把耗时的函数从主线程里分离出来了,还是会导致主线程甚至其他线程响应慢。

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

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

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

    全部都是 while true 、又没有结束条件应该是症结所在
    wangyz1997
        14
    wangyz1997  
    OP
       2020-05-11 11:26:25 +08:00
    @zk8802
    @inframe
    @Drahcir
    感谢,看来要学习一下多进程了
    wangyz1997
        15
    wangyz1997  
    OP
       2020-05-11 11:28:31 +08:00
    @imn1 我在 ThreadSerialComm 里有一个超时 50ms 的 IO 操作(串口通信),是不是这样就和 QTimer 的效果差不多了呢?
    imn1
        16
    imn1  
       
    @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  
       2020-05-11 12:30:11 +08:00
    线程里用信号等待来触发, 不要 while(true) 这样你的线程还是再占用 cpu,需要用 WaitForSingleObject 这些函数, 才能释放当前线程不使用 cpu, 等到信号。
    wangyz1997
        18
    wangyz1997  
    OP
       2020-05-11 12:38:34 +08:00
    @imn1 感谢
    wangyz1997
        19
    wangyz1997  
    OP
       2020-05-11 12:39:51 +08:00
    @jones2000 我的图像处理线程是读取摄像头,然后进行图像算法并将结果 emit 到主线程中。我想法中是让这个线程以最高的速度运行,不需要接受外部的信号之类的,只需要完成它自己的读取-处理-发送就可以了。请问这样该怎么释放 CPU 呢?
    imn1
        20
    imn1  
       2020-05-11 13:00:54 +08:00
    @wangyz1997 #19
    我比较好奇是你每一帧都要捕捉么?不需要长期运行捕捉 thread 的吧

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

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

    另外既然都用 Qt 了,为何不选择直接用 C++ Qt 完成呢?
    weyou
        27
    weyou  
       2020-05-12 00:28:10 +08:00 via Android   1
    @nightwitch 你没有 get 到任何一点啊,Python 的多线程是真的,而且这里是 Qt 的多线程,也是真的。线程切换自然也是操作系统管理的,而不是 Python 解释器。只是 CPU 密集型操作下因为 GIL 的存在,导致每个线程都需要获取到 GIL 才能运行,基本“等价于”线性执行的,但不是你理解的只有一个线程存在。
    enrio
        28
    enrio  
       2020-05-12 09:16:28 +08:00   1
    CPU 密集型+Python+多线程=>多进程
    wangyz1997
        29
    wangyz1997  
    OP
      &nsp;2020-05-12 09:59:20 +08:00
    @defphilip 有一些库只有 Python 有,若是重做轮子太费时费力了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1272 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 17:22 PVG 01:22 LAX 10:22 JFK 13:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86