欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  php教程

以生活例子说明单线程与多线程

程序员文章站 2022-04-11 19:26:20
...

以生活例子说明单线程与多线程

1. 程序设计的目标

在我看来单从程序的角度来看,一个好的程序的目标应该是性能与用户体验的平衡。当然一个程序是否能够满足用户的需求暂且不谈,这是业务层面的问题,我们仅仅讨论程序本身。围绕两点来展开,性能与用户体验。

性能:高性能的程序应该可以等同于CPU的利用率,CPU的利用率越高(一直在工作,没有闲下来的时候),程序的性能越高。
体验:这里的体验不只是界面多么漂亮,功能多么顺手,这里的体验指程序的响应速度,响应速度越快,用户体验越好。

下面我们就这两点进行各种模型的讨论。

2. 单线程多任务无阻塞

以生活中食堂打饭的场景作为比喻,假设有这样的场景,小A,小B,小C 在窗口依次排队打饭。 假设窗口负责打饭的阿姨打一个菜需要耗时1秒。如果小A需要2个菜,小B需要3个菜,小C需要2个菜。如下:

阿姨(CPU):打一个菜需要1秒
小A:2个菜
小B:3个菜
小C:2个菜

那么在这种模型下将所有服务做完阿姨需要耗时 2 + 3 + 2 = 7秒
阿姨 = CPU
小A,小B,小C = 任务(这里是以任务为概念,表示需要做一些事情)
这种模型下CPU是满负荷不间断运转的,没有空闲,用户体验还不错。这种程序中每个任务的耗时都比较小,是非常理想的状态,一般情况下基本不太可能存在。

3. 单线程多任务IO阻塞

将上面的场景稍微做改动:
阿姨:打一个菜需要1秒
小A:2个菜,但是忘记带钱了,要找同学送过来,估计需要等5分钟可以送到(可以理解为磁盘IO)
小B:3个菜
小C:2个菜

这种情况下小A这里发生了阻塞,实际上小A这里耗费了5分钟也就是 300秒+ 2个菜的时间,也就是302秒,而CPU则空闲了300秒,实际上工作2秒。

所有服务做完花费 302 + 3 + 2 = 307秒 CPU实际工作7秒,等待300秒。 极大浪费了CPU的时钟周期。 用户体验很差,因为小A阻塞的时候,后面的所有人都等着,而实际上此时CPU空闲。所以单线程中不要有阻塞出现。

4. 单线程多任务异步IO

还是上面的模型,加入一个角色:值日生小哥,他负责事先询问每一个人是否带钱了,如果带钱了则允许打菜,否则把钱准备好了再说。

值日生小哥问小A准备好打菜了吗,小A说忘带钱了,值日生小哥说,你把钱准备好了再说,小A开始准备(需要300秒,从此刻开始记时)。
值日生小哥问小B准备好打菜了吗,小B说可以了,阿姨服务小B,耗时3秒
值日生小哥问小C准备好打菜了吗,小C说可以了,阿姨服务小C,耗时2秒
值日生小哥问小A准备好了没有,小A说还要等一会,阿姨由于没有人过来服务,处于空闲状态
300秒之后,小A准备好了,阿姨服务小A,耗时2秒

整个过程做完耗时 300 + 2 = 302秒 CPU工作7秒,空闲295秒

值日生小哥相当于select模型中的select功能,负责轮询任务是否可以工作,如果可以则直接工作,否则继续轮询。在小A阻塞的300秒里面,阿姨(CPU)没有傻等,而是在服务后面的人,也就是小B和小C,所以这里与模型3不同的是,这里有5秒CPU是工作的。 如果打饭的人越多,这种模型CPU的利用率越高,例如如果有小D,小E,小F…… 等需要服务,CPU可以在小A阻塞的300秒期间内继续服务其他人。实际上值日生小哥轮询也会耗时,这个耗时是很少的,几乎可以忽略不计,但是如果任务非常多,这个轮询还是会影响性能的,但是epoll模型已经不使用轮询的方式,相当于A,B,C会主动跟值日生小哥报告,说我准备好了,可以直接打菜了。

这种模式下用户体验好,CPU利用率高(任务越多利用率越高)

5. 单线程多任务,有耗时计算

回到最开始的模型,如下:
阿姨:打一个菜需要1秒
小A:200个菜
小B:3个菜
小C:2个菜

顺序做完所有任务,需要耗时 200 + 3 + 2 = 205秒, CPU无空闲,但是用户体验却不是很好,因为显然后面的 B,C 需要等待小A 200秒的时间,这种情况下是没有IO阻塞的,但是任务A本身太耗CPU了,所以说如果单线程中出现了耗时的操作,一定会影响体验(IO操作或者是耗时的计算都属于耗时的操作,都会导致阻塞,但是这两种导致阻塞的性质是不一样的)。在所有的单线程模型中都不允许出现阻塞的情况,如果出现,那么用户体验是极差的,例如在UI编程中(QT,C# Winform)是不允许在UI线程中做耗时的操作的,否则会导致UI界面无响应。 编写Nodejs程序的时候,我们所写的代码实际上是在一个线程中执行的,所以也不允许有阻塞的操作(当然整个Nodejs框架实现异步,一定不止一个线程)。

出现阻塞的情况一般有2种,一种是IO阻塞,例如典型的如磁盘操作,这种情况下的阻塞会导致CPU空闲等待(当然现代操作系统中如果IO阻塞,操作系统一定会将导致IO阻塞的线程挂起)。这种阻塞的情况,可以通过异步IO的方法避免,这样就避免程序中仅有的单线程被操作系统挂起。另一种情况下是确实有非常多的计算操作,例如一个复杂的加密算法,确实需要消耗非常多的CPU时间,这种情况下CPU并不是空闲的,反而是全负荷工作的。这种CPU密集的工作不适合放在单线程中,虽然CPU的利用率很高,但是用户体验并不是很好。这种情况下使用多线程反而会更好,例如如果3个任务,每个任务都在一个线程中,也就是有3个线程,A任务在ThreadA中,B任务在ThreadB中,C任务在ThreadC中,那么即使A任务的计算量比较大,B,C两个任务所在的线程也不必等待A任务完成之后再工作,他们也有机会得到调度,这是由操作系统来完成的。这样就不会因为某一个任务计算量大,而导致阻塞其他任务而影响体验了。

6. 多线程程序

我们将上面的模型改造成多线程的模型是怎样的呢,我们在模型5的基础上添加一个角色,管理员大叔(操作系统的角色):
阿姨:打一个菜需要1秒
小A:200个菜
小B:3个菜
小C:2个菜

加入管理员大叔之后变成这样的了,小A打两个菜之后,大叔说,你打的菜太多了,不能因为你要打200个菜,让后面的同学都没有机会打菜,你打两个菜之后等一会,让后面的同学也有机会。

大叔让小B打两个菜,然后让小C打两个菜(小C完成),然后再让小A打两个菜(完成之后小A总共就有4个菜了),再让小B打1个菜(此时小B总共打3个菜,完成),然后小A打剩下的196个菜。

CPU的利用率:很高,阿姨在不断的工作

用户体验:不错,即使小A要打200个菜,小B,小C也有机会。 当然如果小A说我是帮校长打菜,要快一点(线程优先级高),那也只能先把小A服务完

总耗时: 200 + 3 + 2 + (大叔指挥安排所消耗的时间,包括从小C切换回小A的时候,大叔要知道小A上次打的菜是哪两个,这次应该接着打什么菜,这相当于线程上下文切换的开销以及线程环境的保存与恢复),所以并不是线程越多越好,线程非常多的时候大叔估计会焦头烂额吧,要记住这么状态,切换来切换去也耗时间。

这种模型下实际上是将小A的耗时任务,分成多份去执行而不是集中执行,所以小A要完成他的任务,可能需要更多的时间(期间他也需要等别人,阿姨不会一直为他一个人服务,但是阿姨为他服务的时间是没有变化的),这种其实有点以时间换取用户体验(小B和小C的体验,小A的体验可能就不会那么好了,但是小A本来也非常耗时,所以多等一会是不是也没关系)

那么IO阻塞和CPU计算耗时阻塞这两者有什么区别呢? 区别在于IO阻塞是不使用CPU的,而CPU计算耗时导致的阻塞是会使用CPU的。 例如上面的例子中,小A说忘记带钱了需要同学送钱,于是小A等着同学送钱过来,这个过程中阿姨并没有为小A提供服务,这个过程中为小A提供服务的是他的同学(送钱过来),实际上小A的同学相当于现代计算机系统中的DMA(直接内存操作),小A同学送钱的过程相当于DMA从磁盘读取数据到内存的过程,这个过程基本不需要CPU干预。

当然在DMA技术还没有出现的年代,从磁盘读取文件也是需要CPU发送指令去读取的,也就是说需要CPU的计算,应用到这里的场景中,就是阿姨亲自跑一趟帮小A把钱拿过来。

7. 多CPU

多CPU是一个更加复杂的问题,多CPU如何调度? 小A在第一个窗口打两个菜,又跑到第二个窗口打两个菜这种情况如何处理。小A在第一个窗口,小B在第二个窗口他们要同一个菜,但是这个菜只够一个人,那么两个窗口阿姨如何分配这种需求(实际上应该是由操作系统也就是管理员大叔来决定如何分配,也就是多核下的线程同步与互斥)?

多核CPU情况下,多线程的调度,互斥,锁与同步相对来讲更加复杂,多核情况下是真正的并行,同一时刻有多个线程在同时运行,他们的竞争怎么处理,多个CPU之间如何同步(多CPU之间的缓存状态一致性)等等一系列的问题。

8. 多线程与多进程

上面描述的多线程实际上是讨论的是多线程的调度问题,这里我们说一说多线程与多进程与资源的分配问题。什么意思呢,一群人(多个线程)在一个桌子(进程)上吃饭,他们会涉及到一些问题,比如多个人可能会夹一个菜(竞争),A和B同时看到盘子里面有一块肉,同时伸出筷子去夹,A先夹走,B迟了一点伸到盘子的时候已经没了,只能缩回来(临界资源,互斥),有一个点心需要用馍夹肉一起吃。A夹了肉,B夹了馍,A需要B的馍,B需要A的肉,他们僵持不下谁都不让步(死锁)。

多线程之间的资源共享是非常方便的,因为他们共用进程的资源空间(在一个桌子上),但是需要注意一系列的问题,竞争,死锁,同步等。如果在旁边再开一个桌子(进程)。 那么桌子之间讲话,递东西又不方便(进程间通信),而开一个桌子的开销比在一个桌子上多加一个人的开销要大。另外一个桌子上的人数不可能无限制增加,桌子的容量有限也坐不下这么多人(进程的线程句柄是有限制的)。一个桌子坏了不会影响到另一个桌子上面人的就餐情况(进程间相互独立,一个进程崩溃不会影响另一个),而一个桌子上的某人喝挂了需要送医院,估计这一桌人都要散了(线程挂掉会导致整个进程也挂掉)。所以多线程与多进程是各有优缺点,不能一概而论。

说明:多线程桌子的比喻受到知乎用户[pansz]的启发,但是该比喻似乎说明不了线程同步的情况。

9. 总结

单线程程序:适合IO异步,不能阻塞,不能有大量耗CPU的计算。典型如Nodejs,还有一些网络程序

多线程程序:适合CPU密集型程序

相关标签: 单线程 多线程