开始先引用 https://cloud.tencent.com/developer/article/1495915 的部分内容:
通常多任务的实现,我们都是设计 Master-Worker
,Master
负责分配任务,Worker
负责执行任务,因此多任务环境下,通常是一个 Master
和多个 Worker
。
如果用多进程实现 Master-Worker
,主进程就是 Master
,其他进程就是 Worker
。
如果用多线程实现 Master-Worker
,主线程就是 Master
,其他线程就是 Worker
。
对于多进程,最大的优点就是稳定性高,因为一个子进程挂了,不会影响主进程和其他子进程。当然主进程挂了,所有进程自然也就挂,但主进程只是负责分配任务,挂掉概率非常低。著名的 Apache 最早就是采用多进程模式。
缺点有:
- 创建进程代价大,特别是在 windows 系统,开销巨大,而
Unix/ Linux
系统因为可以调用fork()
,所以开销还行;- 这里的开销还行应该是指Linux下fork有多种形式,而且采用copy-on-write方式,能提高效率
- 操作系统可以同时运行的进程数量有限,会受到内存和 CPU 的限制。
对于多线程,通常会快过多进程,但也不会快太多;缺点就是稳定性不好,因为所有线程共享进程的内存,一个线程挂断都可能直接造成整个进程崩溃。比如在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
进程/线程切换
是否采用多任务模式,第一点需要注意的就是,一旦任务数量过多,效率肯定上不去,这主要是切换进程或者线程是有代价的。
操作系统在切换进程或者线程时的流程是这样的:
- 先保存当前执行的现场环境(CPU寄存器状态、内存页等)
- 然后把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等)
- 开始执行任务
这个切换过程虽然很快,但是也需要耗费时间,如果任务数量有上千个,操作系统可能就忙着切换任务,而没有时间执行任务,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
计算密集型vsI/O密集型
采用多任务的第二个考虑就是任务的类型,可以将任务分为计算密集型和 I/O 密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如对视频进行编码解码或者格式转换等等,这种任务全靠 CPU 的运算能力,虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU 执行任务的效率就越低。计算密集型任务由于主要消耗CPU资源,这类任务用 Python这样的脚本语言去执行效率通常很低,最能胜任这类任务的是C语言,我们之前提到了 Python 中有嵌入 C/C++ 代码的机制。不过,如果必须用 Python 来处理,那最佳的就是采用多进程,而且任务数量最好是等同于 CPU 的核心数。
除了计算密集型任务,其他的涉及到网络、存储介质 I/O 的任务都可以视为 I/O 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 I/O 操作完成(因为 I/O 的速度远远低于 CPU 和内存的速度)。对于 I/O 密集型任务,如果启动多任务,就可以减少 I/O 等待时间从而让 CPU 高效率的运转。一般会采用多线程来处理 I/O 密集型任务。
异步 I/O
现代操作系统对 I/O 操作的改进中最为重要的就是支持异步 I/O。如果充分利用操作系统提供的异步 I/O 支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。Nginx 就是支持异步 I/O的 Web 服务器,它在单核 CPU 上采用单进程模型就可以高效地支持多任务。在多核 CPU 上,可以运行多个进程(数量与CPU核心数相同),充分利用多核 CPU。用 Node.js 开发的服务器端程序也使用了这种工作模式,这也是当下实现多任务编程的一种趋势。
有人觉得奇怪了:单进程单线程怎么执行 ”多任务“ ?
是执行完一个再执行下一个?那不是串行了么?显然串行是低效的,为什么?因为任务通常会有IO操作,当一个任务IO阻塞时,这个线程阻塞了,就不能执行其他任务(因为是单进程单线程)。
传统的单进程单线程无法“并发”执行多任务,现在可以,当然要得益于底层的支持,系统级别的支持,就是异步IO
传统的IO是“同步IO",就是操作系统教材上说的,进程处于”阻塞“状态,我们以“读文件”为例说明。
同步IO: 进程发起“读文件”请求,---调用OS提供的 read 系统调用,系统转入内核态,根据用户的参数,向相关的设备发出“读数据,读多少数据” 的命令,然后把进程停下来,让他到于阻塞队列中,OS去调用其他进程。当设备完成了数据的读入(这期间不需要CPU参与),CPU会收到一个“中断”信号,知道数据OK了,进入相关的中断处理程序,把数据从内核的缓冲区拷贝到用户进程的缓冲区,然后把之前阻塞的进程给唤醒,继续执行。
异步IO:进程发起“读文件”请求,注意,这时候,调用OS提供的另一个系统调用,比如叫 async_read,异步读,这时候,进程不阻塞,继续执行........... 等等!
有个问题,我的程序要去读文件,读了文件,有了数据,然后再做其他的事情,现在呢,文件的数据都还没有,你让我往下执行,我怎么往下执行?
这就涉及“多任务”的编程思想,如果你的程序就是完成“读文件”,再把文件输出,那还需要异步IO干嘛?老老实实的按照原来的方式写代码就行了。你的程序应该是这样:(我们用“写文件”来说明,感受更直观)
#程序
.......
async_write( "myfile" ); //异步写,不阻塞,不等待
check_mouse( )
do other thing
.........
你看到异步IO的作用了,这里的async_write:你可以想象为“存盘”操作,
用户存盘,如果是同步IO,程序执行到这里,肯定要阻塞,因为存盘要花很长一段时间。
但是,现在操作系统提供了这个异步写,程序不用等待,继续往下执行,比如检测鼠标,你感觉不到卡顿
你可以理解为:操作系统会在后台去完成这个IO,和你的程序同时在运行,就像是你的程序使用了多线程一样,一个线程去执行IO操作,一个线程执行其他的。
好了,这就是异步IO的魅力。这里还有个问题,比如我如果读文件,我怎么知道什么时候文件读好了,读完了我要进行某个操作又要怎么写程序呢?
这就要用到“回调函数” callback 这个东西了。
上面,你程序的代码其实往往不是那样的,异步IO操作通常是这样的:
#程序
....
async_write("file", function_finished ); //异步,给出回调函数
....
function_finished: 是一个函数名,通常,我们调用异步IO时,会给一个函数给操作系统,意思是:
当这个事情你做完了,就去执行这个函数function_finished吧, 这样的函数就称为"回调函数"
操作系统会把事件的完成和用户指定的这个函数联系起来,IO完成时,自动去调用函数,不需要用户干预
好了,现在你明白异步IO了,也就知道:单进程单线程怎么执行“多任务”了吧?简单的说,就像是操作系统帮你“重新建了一个线程去执行某个任务”
天下大势,分久必合,合久必分,反反复复,如同多线程,曾经我们是那么热衷,那么欣喜,但是现在,某些场合却又回归到“单线程”-------因为CPU单核速度飞快了,足够快,而线程或进程的切换损失了效率,不划算,而且多线程的编程对大部分人都是极大的考验。
所以,Node.js服务器号称单线程能轻松应对成千上万的网络请求,原因是cpu快,网络请求就是解析http协议这些“简单”的事情,cpu飞快的就完成了一个用户请求的前期处理,这个用户剩下的工作(IO)就给操作系统去完成吧,cpu马不停蹄的去处理下一个用户的请求.......... 这比多线程有太大的优势,因为服务器就一个进程,一个线程,来1万个用户,也就1个线程,和原来的一个用户一个线程的模式相比,要节约多少内存啊!!
那单线程(在网络服务器这块)就这么一统天下了么?不一定,可能随着网络的发展,协议的发展,当网络请求变得计算量比较大的时候,单线程服务器就力不从心了,这时候单线程必然会让用户体验变差(响应慢),以后,随着OS的发展,硬件的发展,可能多线程又重回霸主地位也说不定。