多进程和多线程的较量

开始先引用     https://cloud.tencent.com/developer/article/1495915 的部分内容:

通常多任务的实现,我们都是设计 Master-WorkerMaster 负责分配任务,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的发展,硬件的发展,可能多线程又重回霸主地位也说不定。

发布了12 篇原创文章 · 获赞 5 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/abigriver/article/details/104783559