Python 多进程 多线程 协程 I/O多路复用
引言
在学习Python多进程、多线程之前,先脑补一下如下场景;
说有这么一道题:小红烧水需要10分钟,拖地需要5分钟,洗菜需要5分钟,第一种方式:如果一样一样去干,就是简单的加法,全部做完,需要20分钟;第二种方式:如果在烧水的同时去拖地、洗菜,全部做完,只需要10分钟!也可类比,工作中我们处理日常事务;
可以将上述示例中,做事的主体:人,理解成计算机的CPU,而第二种做事方式,可以简单的理解成多任务!
这里需要了解一下,计算机是由硬件和软件组成的。
硬件中:CPU是计算机的核心,它承担计算机的所有任务。
软件中:操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配、任务的调度。
程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。 每当执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页等!为了保证每个程序的独立性,就需要一个专门的管理和控制执行程序的数据结构——进程控制块。
Come on!一大波概念来袭!!!
进程 VS 线程
什么是进程(Process)?
狭义定义:进程就是一段程序的执行过程。
广义定义:进程是具有一定独立功能的程序,关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。
进程的概念主要有两点:
- 第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程中调用的指令和本地变量。
- 第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
进程的状态: 进程有三个状态,就绪,运行和阻塞。
- 就绪状态其实就是获取了除cpu外的所有资源,只要处理器分配资源马上就可以运行。
- 运行态就是获取了处理器分配的资源,程序开始执行。
- 阻塞态,当程序条件不够时,需要等待条件满足时候才能执行,如等待I/O操作的时候,此刻的状态就叫阻塞态。
进程的特征:
- 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
- 并发性:任何进程都可以同其他进程一起并发执行;
- 独立性:进程是系统进行资源分配和调度的一个独立单位;
- 结构性:进程由程序、数据和进程控制块三部分组成。
什么是线程(thread)?
定义:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程起源:
- 在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
- 后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了,于是就发明了线程。
线程特征:
- 线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),一个线程可以创建和撤销另一个线程;
- 通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
进程 和 线程的区别
1、一个程序至少有一个进程,一个进程至少有一个线程;
进程是资源分配的最小单位,线程是CPU调度的最小单位。
2、资源(内存、寄存器等)分配给进程,进程在执行过程拥有独立的内存空间,而同一进程下的所有线程共享所有资源,从而提高程序的运行效率;
3、处理机分配给线程,即处理机真正运行的是线程;
4、线程在执行过程中,需要协作同步。不同线程间的要利用通信协议来实现同步。
协程
协程,使用一个线程去完成多个任务,可以理解成微线程。
协程的调度完全由用户控制,相对独立,有自己的上下文。一个线程可以有多个协程,用户创建了几个线程,然后每个线程都是循环按照指定的任务清单顺序完成不同的任务,当任务被阻塞的时候执行下一个任务,当恢复的时候再回来执行这个任务,任务之间的切换只需要保存每个任务的上下文内容,就像直接操作栈一样的,这样就完全没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快;另外协程还需要保证是非阻塞的且没有相互依赖,协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;
多进程 VS 多线程
多进程和多线程 是两个不同的概念!!!
什么是多进程?
进程是操作系统进行资源分配的最小单位。
在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多任务。现代的操作系统几乎都是多任务操作系统,能够同时管理多个进程的运行。 多任务带来的好处是明显的,比如你可以边听mp3边上网,与此同时甚至可以将下载的文档打印出来,而这些任务之间丝毫不会相互干扰。
那么这里就涉及到并行的问题,俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用,同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”,它的思想简单介绍如下:在操作系统的管理下,所有正在运行的进程轮流使用CPU,每个进程允许占用CPU的时间非常短(比如10毫秒),这样用户根本感觉不出来 CPU是在轮流为多个进程服务,就好象所有的进程都在不间断地运行一样。但实际上在任何一个时间内有且仅有一个进程占有CPU。
假如一台计算机有多个CPU,如果进程数小于CPU数,则不同的进程可以分配给不同的CPU来运行,这样,多个进程就是真正同时运行的,这便是并行;但如果进程数大于CPU数,则仍然需要使用并发技术。
什么是多线程?
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。
最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。
线程处理非常简单,但必须注意一个问题:共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就可能会遇到问题。对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源。
一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。
多进程 多线程 的优缺点
多进程和多线程,是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。
要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。
- 如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
- 如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。
多进程 优点 是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程 缺点 是创建进程的代价大,在Unix/Linux系统下,用
fork
调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。还有一个不足之处,多进程下程序与各进程之间的通信和数据共享不方便;多线程 优点 通常比多进程快一点,但是也快不到哪去,开销小!
多线程主要是为了节约CPU时间,为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率!
多线程 缺点 任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
额外补充
实际开发中 什么时候用多线程? 什么时候用多进程?
在TCP/IP协议族中,使用频率最高的上层协议无非是TCP,UDP,HTTP这3者了。
其中HTTP用在B/S架构通信中,前两种协议用在C/S架构中。
TCP是面向连接的,UDP面向非连接。同时,多进程间是资源隔离的,多线程间是资源共享的。
基于以上原因,从程序设计难易程度上说:
采用TCP协议的软件用多进程更简单,采用UDP协议的软件二者都可。
这并不是唯一的,只是基于各人的经验,以及技术特性导致的复杂程度不同作为参考标准。
实质上,不管采用什么协议,采用任何多任务技术都可以在理论上实现。然而,软件追求的是高效和稳定,所以在实际软件设计中,便会参考软件的实现难度和维护性等指标。
下面根据经验简单阐述协议与多任务方式的组合特点:
1. TCP+多进程,由于TCP是面向连接的,而一个服务器的单进程空间内如果多个子任务都能看到连接的话,保存连接的资源会增多,同时各资源维护难度也随之加大。故而采用多进程的话(多线程间资源是共享的),子进程各自独享资源,每个连接对应一个子进程,这样进程之间的资源是自然隔离的,这就省去了管理的代价。
2. UDP+多线程,在UDP程序中,服务端采用多进程还是多线程都可。因为UDP面向非连接,就省去了管理每个连接的成本,这样的话采用多线程即可。但实际情况需要根据特性而定。
并发 VS 并行
假如一台计算机有多个CPU,如果进程数小于CPU数,则不同的进程可以分配给不同的CPU来运行,这样,多个进程就是真正同时运行的,这便是并行。但如果进程数大于CPU数,则仍然需要使用并发技术。
在Windows中,进行CPU分配是以线程为单位的,一个进程可能由多个线程组成,这时情况更加复杂,但简单地说,有如下关系:
总线程数<= CPU数量:并行运行
总线程数> CPU数量:并发运行
多任务操作系统(如Windows)的基本原理是:操作系统将CPU的时间片分配给多个线程,每个线程在操作系统指定的时间片内完成(注意,这里的多个线程是分属于不同进程的)。操作系统不断的从一个线程的执行切换到另一个线程的执行,如此往复,宏观上看来,就好像是多个线程在一起执行。由于这多个线程分属于不同的进程,因此在我们看来,就好像是多个进程在同时执行,这样就实现了多任务。
虽然有的时候我们用多任务并发,充分发挥CPU等资源,来达到快速处理的任务的目的,但是是不是并发量越大越快呢,答案显然是不对的。
我们打个比方,假设你正在准备考试,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
I/O 编程
IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output,随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫Input。
所以,通常,程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。
IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。对于浏览网页来说,浏览器和新浪服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。
由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:
第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;
另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。
很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂,异步IO的复杂度远远高于同步IO。
操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。
计算密集型 VS I/O密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
同步 VS 异步
同步:
同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
按照这个定义,其实绝大多数函数都是同步调用(例如sin isdigit等)。
但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。
最常见的例子就是 SendMessage。
该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。
当对方处理完毕以后,该函数才把消息处理函数所返回的值返回给调用者。
异步的概念和同步相对
当一个异步过程调用发出后,调用者不会立刻得到结果。
实际处理这个调用的部件是在调用发出后,
通过状态、通知来通知调用者,或通过回调函数处理这个调用。考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。
阻塞 VS 非阻塞
阻塞/非阻塞, 它们是程序在等待消息(无所谓同步或者异步)时的状态。
阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。
对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。socket接收数据函数recv是一个阻塞调用的例子。
当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
非阻塞
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
易混淆的点:
很多人也会把异步和非阻塞混淆,
因为异步操作一般都不会在真正的IO 操作处被阻塞,
比如如果用select 函数,当select 返回可读时再去read 一般都不会被阻塞
就好比当你的号码排到时一般都是在你之前已经没有人了,所以你再去柜台办理业务就不会被阻塞.
可见,同步/异步与阻塞/非阻塞是两组不同的概念,它们可以共存组合,
而很多人之所以把同步和阻塞混淆,我想也是因为没有区分这两个概念,
比如阻塞的read/write 操作中,其实是把消息通知和处理消息结合在了一起,
在这里所关注的消息就是fd 是否可读/写,而处理消息则是对fd 读/写.
当我们将这个fd 设置为非阻塞的时候,read/write 操作就不会在等待消息通知这里阻塞,
如果fd 不可读/写则操作立即返回.
前方高能 非战斗人员迅速撤离 。。。。。
同步/异步与阻塞/非阻塞的组合分析
_____|_____阻塞_______|_____非阻塞_____
同步 | 同步阻塞 | 同步非阻塞
异步 | 异步阻塞 | 异步非阻塞
同步阻塞形式:
效率是最低的,
拿上面的例子来说,就是你专心排队,什么别的事都不做。
实际程序中
就是未对fd 设置O_NONBLOCK 标志位的read/write 操作,
异步阻塞形式:
如果在银行等待办理业务的人采用的是异步的方式去等待消息被触发,也就是领了一张小纸条,
假如在这段时间里他不能离开银行做其它的事情,那么很显然,这个人被阻塞在了这个等待的操作上面;
异步操作是可以被阻塞住的,只不过它不是在处理消息时阻塞,而是在等待消息被触发时被阻塞.
比如select 函数,
假如传入的最后一个timeout 参数为NULL,那么如果所关注的事件没有一个被触发,
程序就会一直阻塞在这个select 调用处.
同步非阻塞形式:
实际上是效率低下的,
想象一下你一边打着电话一边还需要抬头看到底队伍排到你了没有,
如果把打电话和观察排队的位置看成是程序的两个操作的话,
这个程序需要在这两种不同的行为之间来回的切换,效率可想而知是低下的;
很多人会写阻塞的read/write 操作,
但是别忘了可以对fd 设置O_NONBLOCK 标志位,这样就可以将同步操作变成非阻塞的了;
异步非阻塞形式:
效率更高,
因为打电话是你(等待者)的事情,而通知你则是柜台(消息触发机制)的事情,
程序没有在两种不同的操作中来回切换.
比如说,这个人突然发觉自己烟瘾犯了,需要出去抽根烟,
于是他告诉大堂经理说,排到我这个号码的时候麻烦到外面通知我一下(注册一个回调函数),
那么他就没有被阻塞在这个等待的操作上面,自然这个就是异步+非阻塞的方式了.
如果使用异步非阻塞的情况,
比如aio_*组的操作,当发起一个aio_read 操作时,函数会马上返回不会被阻塞,
当所关注的事件被触发时会调用之前注册的回调函数进行处理,
Python多线程、多进程实现:http://www.cnblogs.com/hellojesson/p/6397173.html
Python操作异步IO\队列\缓存:http://www.cnblogs.com/alex3714/articles/5248247.html
IO多路复用的几种实现机制的分析: http://blog.csdn.net/zhang_shuai_2011/article/details/7675797
知乎关于I/O多路复用的解答:https://www.zhihu.com/question/32163005