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

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

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

<1> 值日生小哥问小A准备好打菜了吗,小A说忘带钱了,值日生小哥说,你把钱准备好了再说,小A开始准备(须要300秒,今后刻开始记时)。
<2> 值日生小哥问小B准备好打菜了吗,小B说能够了,阿姨服务小B,耗时3秒 (与此同时小A还在准备中,而且已经准备了3秒)
<3> 值日生小哥问小C准备好打菜了吗,小C说能够了,阿姨服务小C,耗时2秒 (与此同时小A还在准备中,而且已经准备了5秒,前面的3秒+这里的2秒)
<4> 值日生小哥问小A准备好了没有,小A说还要等一会,阿姨因为没有人过来服务,处于空闲状态 (小A还在准备中,他还须要准备295秒,可是这个时候B和C已经服务完了)
<5> 从第1步开始计时有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]的启发,可是该比喻彷佛说明不了线程同步的状况。 spa

9. 总结

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

 

若是您以为这篇文章对您有帮助,须要您的【赞】,让更多的人也能看见哦

相关文章
相关标签/搜索