当前位置: 首页 > news >正文

为什么你的程序跑不满CPU?——简单聊聊多核多线程

最近同事测试自己的程序,感觉处理耗时太长,一看CPU使用率,才25%。想要提高CPU使用率降低处理时长,于是向我询问。以此为契机写了这篇,聊聊多核多线程。水平有限,仅供参考。

1.单核单线程

一切开始的前提是,你需要知道,CPU执行的所有代码其实就是一条条指令。

首先来聊聊单核单线程下你的程序是怎么运行的。假如你的程序就两行代码:

b=a+1;

c=b+1;

而你的CPU每运行一行代码需要1秒,那么很明显,对于单核CPU来说,运行你的代码需要2秒。但实际上这往往需要2秒多,因为你的CPU还需要处理很多可能的中断,比如当你的CPU刚执行完b=a+1时, 这个时候插入了一台USB设备,它触发了一个中断,中断你可以简单理解为一个函数,这里的USB中断你可以理解为,插入了USB就要执行一个“USB函数”,这个过程不受控制(实际过程复杂的多)。一般来说中断优先级是高于你的用户代码的,所以CPU只能转头去执行USB函数中的代码,这个时候你的代码才执行到了一半,当执行完USB函数后,CPU才会转头回来继续执行你的代码。最终你的代码可能运行了3秒甚至更久。

2.单核多线程

你可能觉得2秒甚至更久的时间对你的程序来说太长了,那是否可以使用多线程优化程序的执行?

其实仔细想一下就知道,无论使用多少个线程,假如CPU每执行一行代码就是1秒,那这两行代码怎么也得要2秒。所以单核情况下,多线程并不会提高代码执行效率。单核多线程的意义在于,当你的代码中有需要等待的地方,CPU可以在等待时去做其他事,这里你需要知道,CPU调度的最小单位是线程。比如上文中的例子假如是这样:

b=a+1;

sleep(1);

d=c+1;

正常你用单个线程去跑这段代码假如需要3秒,最后我们需要的结果是b和d,但很明显d=c+1在这里需要等待sleep休眠1秒后才能计算,而实际上这行代码和前文并没有数据上的关联和依赖。如果此时我们用两个线程,线程1跑【b=a+1;sleep(1)】,线程2跑【d=c+1】,虽然我们的CPU是单核的,意味着同一时间只能跑一行代码,但是在线程1运行完b=a+1后,sleep时,CPU可以转去执行d=c+1,这样最终我们只用了2秒就可以得到b和d的结果,此时多线程的运用相当于填补了CPU执行sleep等类似的等待过程。
单核多线程的另外一个意义是可以做到多个线程“同时”进行,注意这里的同时是加引号的。这种情况下每个线程如果优先级相同,则它们可能消耗相同的时间片,你可以把时间片简单理解为每个线程每次能够运行的最大时间。这适用于对时间不敏感,但是强调了同时的程序。比如你的代码是这样的:

b=a+1;

d=c+1;

f=e+1;

h=g+1;

很明显这段代码运行需要最少4秒,上文我说过,使用多线程并不能缩短这个时间,但是你希望【b=a+1;d=c+1】和【f=e+1;h=g+1】这两段代码“同时”执行,这种情况下使用多线程也是可以的,假如时间片=1秒,如果把这两段代码分别放在两个线程,且它们优先级相同,则CPU的执行顺序可能是这样的:

第一秒:先执行线程1的b=a+1;

第二秒:线程1的时间片消耗完了,CPU转去执行线程2的f=e+1;

第三秒:线程2的时间片消耗完了,CPU转去执行线程1的d=c+1;

第四秒:线程1的时间片又消耗完了,CPU又转去执行线程2的h=g+1;

最终CPU还是消耗了4秒执行上述代码,但是却是两个线程交替执行。这在某些场景,比如播放电影(我们需要播放画面的同时,播放音频)时,是很有用的。

单核情况下的多线程叫做并发。也就是所谓的单核多线程其实只是在同一个CPU上交替执行多个线程,但实际是,在任意时间点,只能有一个线程执行,只不过CPU切换的速度很快,给你造成一种多个线程同时运行的假象。只有多核才能做到真正意义上的同时运行。更多细节可以搜索并发并行的区别。

3.多核单线程

假如你有一个四核CPU,每个核还是1秒执行1行代码,而你的代码是:

b=a+1;

c=b+1;

e=d+1;

f=e+1;

如果把这四行代码放在一个mian函数里执行,你会发现CPU在程序执行的时候,只有25%。这是否是因为有3个核压根就没有工作?

其实不然,这种情况下,无论是只使用1个核还是四个核都使用,CPU使用率最多都是25%。为什么?

假如你的程序只放在第一个核上运行,这很好理解:

表1
时间CPU0使用率CPU1使用率CPU2使用率CPU3使用率
第1秒100%0%0%0%
第2秒100%0%0%0%
第3秒100%0%0%0%
第4秒100%0%0%0%

4秒内,CPU实际可以执行16行代码,而实际只有CPU0执行了4行,所以CPU在4秒内总的使用率为4/16=25%。

假如你的程序在4个核上运行,则可能是:

表2
时间CPU0使用率CPU1使用率CPU2使用率CPU3使用率
第1秒100%0%0%0%
第2秒0%100%0%0%
第3秒0%0%100%0%
第4秒0%0%0%100%

同样的也是25%。你可以能会好奇为何CPU不是按下面的方式运行的:

表3
时间CPU0使用率CPU1使用率CPU2使用率CPU3使用率
第1秒100%100%100%100%
第2秒0%0%0%0%
第3秒0%0%0%0%
第4秒0%0%0%0%

在这种情况下,我们统计CPU占用率的周期很重要,如果以4秒为一个周期,那CPU使用率还是25%,如果以1秒为周期则第一秒使用率是100%,后面3秒使用率都是0%。但我们最关心的还是这种情况下可以把程序执行需要4秒给缩短到1秒。但这种情况是不可能发生的,当你使用单线程写了一个程序时,就注定了你的每行代码都要依次执行(这里不考虑CPU乱序执行,且就算乱序也不影响),你的第二行代码就必须等第一行代码执行完毕才能执行,无论第一行代码或是第二行代码在哪个CPU上执行!所以,无论以哪种情况来说,你的CPU从你的代码开始到结束,占用率最多25%,且必须耗时4秒甚至更久才能执行完。

4.多核多线程

按照上面的说法,我们是否可以把4行代码分别放在四个线程中,这样岂不是可以实现表3?

实际是可以实现的,但可惜的是,计算结果是错误的。因为我们可以看到,4行代码里面,数据是有依赖关系的,计算c之前需要保证先计算b,计算b之前,需要保证a是正确的。这就是所谓的线程间同步。所以即使是使用4个线程,为了使计算结果正确,我们也没有办法做到耗时1秒。

在这个例子中,假如我们把四行代码分别放在四个线程中,然后四个线程分别在4个核上运行,比如第一个核上运行线程1,执行的是第一行代码,以此类推。

那我们的代码在实际考虑数据依赖关系后,很可能是:

第一秒,线程1执行,其他线程都等待,也就是虽然其他线程放在其他核上,但是其他核使用率仍然为0%。

第二秒,线程1执行完毕,告诉了线程2,然后线程2开始执行,此时CPU0、2、3核都没在使用。

第三秒,线程2执行完毕,告诉了线程3,然后线程3开始执行,此时CPU0、1、3核都没在使用。

第四秒,线程3执行完毕,告诉了线程4,然后线程4开始执行,此时CPU0、1、2核都没在使用。

其中一个线程如何告诉另一个线程可以搜索线程(进程)间同步方式相关资料。

我们看下来,发现好像使用多线程也并没有缩小耗时,也就是说,如果你的程序执行有数据依赖关系的,多线程并不能优化执行效率,因为你后面的代码即使可以执行,也无法执行,因为它要等待前面的代码计算完成,用前面的计算结果继续计算。

细心的你可能发现,上面例子中第三行代码并不需要等待第二行代码执行完毕,因为c=b+1和e=d+1并没有关系,也就是第二行代码必须等第一行计算完毕,第四行代码必须等第三行计算完毕,但是前两行和后两行并没有一点关系,那我们完全可以把第1、2行代码放在线程1中,运行在其中一个核上,把第3、4行代码放在线程2中,运行在另一个核上,最终CPU使用率是:

表4
时间CPU0使用率CPU1使用率CPU2使用率CPU3使用率
第1秒100%100%0%0%
第2秒100%100%0%0%
第3秒0%0%0%0%
第4秒0%0%0%0%

之前的多核单线程中,程序需要运行4秒,总体CPU使用率25%,现在程序只需要运行2秒,总体CPU使用率50%。此时才把多核多线程优势完全发挥出来。

这是否就是多线程优化极限了?

其实在特殊情况下,还可以再次优化。

多级流水线

很多情况下,我们的程序需要处理的数据是流式的,比如音视频,也就是数据会每隔一定时间来一帧,而我们的程序需要每来一帧处理一次,这个时候如果数据来的时间大于数据处理的时间,我们可以仿照CPU的多级流水线的设计思路优化我们的处理。 

按照我们上文的例子,假如我们的处理流程是这样的:

在这里,a和d每隔2秒来一次的话是可以正常输出c和f的值的,如果a和d每隔1秒来一次的话,等到下一次数据来,我们上次数据还没有计算完成,肯定会卡顿,导致数据丢失,而且这种情况无法通过缓存输入数据解决,因为这会越堆积越多。但是如果我们按照下面的思路修改:

程序运行流程是这样的:

第一秒:第一帧数据到来,线程1和线程3执行,因为没有线程1和3的结果,线程2和线程4运行跳过,最终没有输出c、f,CPU使用率50% 

第二秒:第二帧数据到来,线程1和线程3计算第二次来的数据,线程2和线程4计算上一轮中线程1和线程3输出的数据,最终输出第一帧数据对应的c、f,CPU使用率100%

第三秒:第三帧数据到来,线程1和线程3计算第三次来的数据,线程2和线程4计算上一轮中线程1和线程3输出的数据,最终输出第二帧数据对应的c、f,CPU使用率100%

余下同理。

由上可以看出,通过错位一帧,我们可以把CPU使用率从原来的50%提高到100%,虽然输出落后输入一帧,但是却可以满足1秒输出一次的要求。使用此种方法需要考虑三个问题,一是整个过程耗时、被分割为每一级的耗时、与输入间隔之间是否满足要求,理论上分割级数越多,整体耗时越短。二是分割级数越多,输入与输出之间的迟滞越长。三是因为每一级都要保存上次计算的结果(有时候我们可能并不是像这个例子一样那么简单,可能每级之间都使用队列通信,此时缓存的数据不止一帧),所以分级越多,内存消耗越大。

相关文章:

  • 使用windows系统给C盘分盘
  • 外包四年太差劲,幡然醒悟要跳槽
  • 合并字符串-指针
  • 世界上最伟大最邪恶的软件发明
  • 设计模式~简单工厂模式
  • 羊没羊,好像也没那么重要了!
  • C语言必背18个经典程序
  • UG/NX二次开发Siemens官方NXOPEN实例解析—2.6 CreateNote
  • 斯坦福联合Meta提出多模态模型RA-CM3,检索增强机制或成文本图像领域新制胜法宝
  • Mit6.006-problemSession03
  • 高通Ride软件开发包使用指南(12)
  • 回调函数的基本使用
  • 艾美捷内皮血管生成检测试剂盒的多种特点
  • Java反射介绍
  • 【Spring专题】「开发指南」夯实实战基础功底之解读logback-spring.xml文件的详解实现
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • ES学习笔记(12)--Symbol
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • Spring框架之我见(三)——IOC、AOP
  • vue从入门到进阶:计算属性computed与侦听器watch(三)
  • zookeeper系列(七)实战分布式命名服务
  • 盘点那些不知名却常用的 Git 操作
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 我的业余项目总结
  • 写代码的正确姿势
  • 一、python与pycharm的安装
  • 用mpvue开发微信小程序
  • 云大使推广中的常见热门问题
  • AI又要和人类“对打”,Deepmind宣布《星战Ⅱ》即将开始 ...
  • zabbix3.2监控linux磁盘IO
  • 阿里云重庆大学大数据训练营落地分享
  • 教程:使用iPhone相机和openCV来完成3D重建(第一部分) ...
  • 昨天1024程序员节,我故意写了个死循环~
  • #我与Java虚拟机的故事#连载04:一本让自己没面子的书
  • #我与Java虚拟机的故事#连载06:收获颇多的经典之作
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (免费领源码)Python#MySQL图书馆管理系统071718-计算机毕业设计项目选题推荐
  • (十五)Flask覆写wsgi_app函数实现自定义中间件
  • (一)WLAN定义和基本架构转
  • (转载)CentOS查看系统信息|CentOS查看命令
  • (转载)OpenStack Hacker养成指南
  • **登录+JWT+异常处理+拦截器+ThreadLocal-开发思想与代码实现**
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • /bin/bash^M: bad interpreter: No such file ordirectory
  • @Pointcut 使用
  • @我的前任是个极品 微博分析
  • [52PJ] Java面向对象笔记(转自52 1510988116)
  • [8-27]正则表达式、扩展表达式以及相关实战
  • [BSGS算法]纯水斐波那契数列
  • [bzoj 3534][Sdoi2014] 重建
  • [CISCN2019 华东南赛区]Web4