进程 vs 线程
这一节我们讨论多进程和多线程的优缺点。
首先要实现多任务,我们需要设计Master-Worker模式,Master负责分配任务,Worker负责执行任务。因此,多任务环境下,通常是一个Master,多个Wroker。
如何用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker.
如何使用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker.
多进程最大的优点是稳定性高,因为一个子进程崩溃了不影响其他主进程和子进程。当然如果主进程挂掉,就都挂了。著名的Apache最早就采用的多进程模式。
多进程的缺点是创建进程的代价大,在Linux下采用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统同时运行的进程数是有限的,在内存的CPU的限制下,同时有几千个进程在跑,操作系统调度都是问题。
多线程模式通常比多进程快一点,但是也快不到哪去。多线程最大的缺点是一个线程挂掉都可能直接造成整个进程的崩溃。因为所有线程共享进程的内存。在Windows上,如果一个线程的运行代码出现问题,你经常会看到这样的提示“该程序执行了非法操作,即将关闭。”,其实往往是某个线程出了问题,操作系统强行关闭整个进程。
在windows下,多线程执行效率比多进程高,所以微软的IIS服务器默认采用多线程模式,由于多线程存在稳定性问题,IIS的稳定性就不如Apache。为了解决这个问题,IIS和Apache又有多进程和多线程混合模式,真是越搞越复杂。
线程切换
无论多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,你正在准备中考,需要复习语文、数学、英语物理、化学这五科。每科需耗时1小时。
如果你先花一小时复习完语文,花一小时复习完数学,这样依次复习完全部课程,这种方式称为单任务模型或批处理模型。
假设你准备切换到多任务模型,先做1分钟语文,切换到数学,在做一分钟数学。这一种方式就像单核CPU执行多任务是一样的了。
但是切换有代价,操作系统切换线程,它需要先保存当前执行的现场环境(CPU寄存器状态、寄存页等),然后把新任务执行环境准备好,才能开始执行。如果要有几千个任务同时执行,操作系统可能就只忙着切换任务,根本没多少时间切换任务。这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦到一个限度,就会消耗掉系统的所有资源,结果效率急剧下降,所有任务都做不好。
计算密集型 vs IO密集型
是否采用多任务的第二个考虑因素是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型主要进行计算,如计算圆周率、对视频进行高清解码等。为了达到CPU高效利用率,计算密集型任务同时进行的数量应等于CPU的核心数。
计算密集型任务主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的语言运行效率很低,完全不合适计算密集型任务,这种任务最好用C语言编写。
第二种是IO密集型,主要涉及到网络硬盘、磁盘IO读取的任务都是IO密集型。这种任务表现为CPU消耗的少,主要等待IO操作完成(IO的速度远远低于CPU和内存的速度)。对于IO任务,任务越多,CPU利用率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务运行期间,99%的时间都花费在IO上,花在CPU上的时间很少,因此用速度极快的C语言替换运行速度极低的Python完全起不到提升运行效率。最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选。C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都是在等待IO,单进程单线程都会导致其他任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统最大的改进就是支持异步IO。如果充分利用异步IO,就可以用单进程单线程来执行多任务,这种全新的模型成为事件驱动模型,Nginx就是支持异步IO的Web服务器。它在单核CPU上采用单进程模型就可以高效的执行多任务,在多核CPU上,可以运行多个进程(与CPU核数相同),充分利用多核CPU。
由于系统的进程数量十分有限,因此,操作系统调度非常有效,用异步IO编程模型来执行多任务是一个趋势。
对应到Python语言,单进程的异步编程成为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后边讨论如何编写协程。