协程,又称微线程,纤程。英文名Coroutine。html
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中获得普遍应用。python
子程序,或者称为函数,在全部语言中都是层级调用,好比A调用B,B在执行过程当中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。react
因此子程序调用是经过栈实现的,一个线程就是执行一个子程序。linux
子程序调用老是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不一样。nginx
协程看上去也是子程序,但执行过程当中,在子程序内部可中断,而后转而执行别的子程序,在适当的时候再返回来接着执行。程序员
线程和进程的操做是由程序触发系统接口,最后的执行者是系统;协程的操做则是程序员。web
协程能够被认为是一种用户空间线程,与传统的抢占式线程相比,有2个主要的优势:ajax
协程存在的意义:对于多线程应用,CPU经过切片的方式来切换线程间的执行,线程切换时须要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。算法
协程的适用场景:当程序中存在大量不须要CPU的操做时(IO),适用于协程;数据库
协程的好处:
缺点:
进程:
进程之间不共享任何状态,进程的调度由操做系统完成,每一个进程都有本身独立的内存空间,进程间通信主要是经过信号传递的方式来实现的,实现方式有多种,信号量、管道、事件等,任何一种方式的通信效率都须要过内核,致使通信效率比较低。因为是独立的内存空间,上下文切换的时候须要保存先调用栈的信息、cpu各寄存器的信息、虚拟内存、以及打开的相关句柄等信息,因此致使上下文进程间切换开销很大,通信麻烦。
线程:
线程之间共享变量,解决了通信麻烦的问题,可是对于变量的访问须要锁,线程的调度主要也是有操做系统完成,一个进程能够拥有多个线程,可是其中每一个线程会共享父进程像操做系统申请资源,这个包括虚拟内存、文件等,因为是共享资源,因此建立线程所须要的系统资源占用比进程小不少,相应的可建立的线程数量也变得相对多不少。线程时间的通信除了能够使用进程之间通信的方式之外还能够经过共享内存的方式进行通讯,因此这个速度比经过内核要快不少。另外在调度方面也是因为内存是共享的,因此上下文切换的时候须要保存的东西就像对少一些,这样一来上下文的切换也变得高效。
协程:
协程的调度彻底由用户控制,一个线程能够有多个协程,用户建立了几个线程,而后每一个线程都是循环按照指定的任务清单顺序完成不一样的任务,当任务被堵塞的时候执行下一个任务,当恢复的时候再回来执行这个任务,任务之间的切换只须要保存每一个任务的上下文内容,就像直接操做栈同样的,这样就彻底没有内核切换的开销,能够不加锁的访问全局变量,因此上下文的切换很是快;另外协程还须要保证是非堵塞的且没有相互依赖,协程基本上不能同步通信,多采用一步的消息通信,效率比较高。
进程、线程与协程
从硬件发展来看,从最初的单核单CPU,到单核多CPU,多核多CPU,彷佛已经到了极限了,可是单核CPU性能却还在不断提高。server端也在不断的发展变化。若是将程序分为IO密集型应用和CPU密集型应用,两者的server的发展以下:
IO密集型应用: 多进程->多线程->事件驱动->协程
CPU密集型应用:多进程-->多线程
调度和切换的时间:进程 > 线程 > 协程
不须要实现复杂的内存共享且需利用多cpu,用多进程;实现复杂的内存共享及IO密集型应用:多线程或协程;实现复杂的内存共享及CPU密集型应用:协程
咱们首先来简单回顾一下一些经常使用的网络编程模型。网络编程模型能够大致的分为同步模型和异步模型两类。
同步模型使用阻塞IO模式,在阻塞IO模式下调用read等IO函数时会阻塞线程直到IO完成或失败。 同步模型的典型表明是thread_per_connection模型,每当阻塞在主线程上的accept调用返回时则建立一个新的线程去服务于新的socket的读/写。这种模型的优势是程序逻辑简洁,符合人的思惟;缺点是可伸缩性收到线程数的限制,当链接愈来愈多时,线程也愈来愈多,频繁的线程切换会严重拖累性能,同时不得不处理多线程同步的问题。
异步模型通常使用非阻塞IO模式,并配合epoll/select/poll等多路复用机制。在非阻塞模式下调用read,若是没有数据可读则当即返回,并通知用户没有可读(EAGAIN/EWOULDBLOCK),而非阻塞当前线程。异步模型能够使一个线程同时服务于多个IO对象。 异步模型的典型表明是reactor模型。在reactor模型中,咱们将全部要处理的IO事件注册到一个中心的IO多路复用器中(通常为epoll/select/poll),同时主线程阻塞在多路复用器上。一旦有IO事件到来或者就绪,多路复用器返回并将对应的IO事件分发到对应的处理器(即回调函数)中,最后处理器调用read/write函数来进行IO操做。
异步模型的特色是性能和可伸缩性比同步模型要好不少,可是其结构复杂,不易于编写和维护。在异步模型中,IO以前的代码(IO任务的提交者)和IO以后的处理代码(回调函数)是割裂开来的。
协程的出现出现为克服同步模型和异步模型的缺点,并结合他们的优势提供了可能: 如今假设咱们有3个协程A,B,C分别要进行数次IO操做。这3个协程运行在同一个调度器或者说线程的上下文中,并依次使用CPU。调度器在其内部维护了一个多路复用器(epoll/select/poll)。 协程A首先运行,当它执行到一个IO操做,但该IO操做并无当即就绪时,A将该IO事件注册到调度器中,并主动放弃CPU。这时调度器将B切换到CPU上开始执行,一样,当它碰到一个IO操做的时候将IO事件注册到调度器中,并主动放弃CPU。调度器将C切换到cpu上开始执行。当全部协程都被“阻塞”后,调度器检查注册的IO事件是否发生或就绪。假设此时协程B注册的IO时间已经就绪,调度器将恢复B的执行,B将从上次放弃CPU的地方接着向下运行。A和C同理。 这样,对于每个协程来讲,它是同步的模型;可是对于整个应用程序来讲,它是异步的模型。
编程范式(Programming Paradigm)是某种编程语言典型的编程风格或者说是编程方式。随着编程方法学和软件工程研究的深刻,特别是OO思想的普及,范式(Paradigm)以及编程范式等术语渐渐出如今人们面前。面向对象编程(OOP)经常被誉为是一种革命性的思想,正由于它不一样于其余的各类编程范式。编程范式也许是学习任何一门编程语言时要理解的最重要的术语。
托马斯.库恩提出“科学的革命”的范式论以后,Robert Floyd在1979年图灵奖的颁奖演说中使用了编程范式一词。编程范式通常包括三个方面,以OOP为例:
简单的说,编程范式是程序员看待程序应该具备的观点。
为了进一步加深对编程范式的认识,这里介绍几种最多见的编程范式。
须要再次提醒注意的是:编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种编程语言也能够适用多种编程范式。
过程化编程,也被称为命令式编程,应该是最原始的、也是咱们最熟悉的一种传统的编程方式。从本质上讲,它是“冯.诺伊曼机“运行机制的抽象,它的编程思惟方式源于计算机指令的顺序排列。
(也就是说:过程化语言模拟的是计算机机器的系统结构,而并非基于语言的使用者的我的能力和倾向。这一点咱们应该都很清楚,好比:咱们最先曾经使用过的单片机的汇编语言。)
过程化编程的步骤是:
首先,咱们必须将待解问题的解决方案抽象为一系列概念化的步骤。而后经过编程的方式将这些步骤转化为程序指令集(算法),而这些指令按照必定的顺序排列,用来讲明如何执行一个任务或解决一个问题。这就意味着,程序员必需要知道程序要完成什么,而且告诉计算机如何来进行所需的计算工做,包括每一个细节操做。简言之,就是将计算机看做一个有始有终服从命令的装置。
因此在过程化编程中,把待解问题规范化、抽象为某种算法是解决问题的关键步骤。其次,才是编写具体算法和完成相应的算法实现问题的正确解决。固然,程序员对待解问题的抽象能力也是很是重要的因素,但这自己已经与编程语言无关了。
程序流程图是过程化语言进行程序编写的有效辅助手段。
尽管现存的计算机编程语言不少,可是人们把全部支持过程化编程范式的编程语言都被概括为过程化编程语言。例如机器语言、汇编语言、BASIC、COBOL、C 、FORTRAN、语言等等许多第三代编程语言都被概括为过程化语言。
过程化语言特别适合解决线性(或者说循序渐进)的算法问题。它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式很是相似咱们的工做和生活方式,由于咱们的平常活动都是循序渐进的顺序进行的。
过程化语言趋向于开发运行较快且对系统资源利用率较高的程序。过程化语言很是的灵活并强大,同时有许多经典应用范例,这使得程序员能够用它来解决多种问题。
过程化语言的不足之处就是它不适合某些种类问题的解决,例如那些非结构化的具备复杂算法的问题。问题出如今,过程化语言必须对一个算法加以详尽的说明,而且其中还要包括执行这些指令或语句的顺序。实际上,给那些非结构化的具备复杂算法的问题给出详尽的算法是极其困难的。
普遍引发争议和讨论的地方是:无条件分支,或goto语句,它是大多数过程式编程语言的组成部分,反对者声称:goto语句可能被无限地滥用;它给程序设计提供了制造混 乱的机会。目前达成的共识是将它保留在大多数语言中,对于它所具备的危险性,应该经过程序设计的规定将其最小化。
其实,基于事件驱动的程序设计在图形用户界面(GUI)出现好久前就已经被应用于程序设计中,但是只有当图形用户界面普遍流行时,它才逐渐形演变为一种普遍使用的程序设计模式。
在过程式的程序设计中,代码自己就给出了程序执行的顺序,尽管执行顺序可能会受到程序输入数据的影响。
在事件驱动的程序设计中,程序中的许多部分可能在彻底不可预料的时刻被执行。每每这些程序的执行是由用户与正在执行的程序的互动激发所致。
事件驱动经常用于用户与程序的交互,经过图形用户接口(鼠标、键盘、触摸板)进行交互式的互动。固然,也能够用于异常的处理和响应用户自定义的事件等等。
事件的异常处理比用户交互更复杂。
事件驱动不只仅局限在GUI编程应用。可是实现事件驱动咱们还须要考虑更多的实际问题,如:事件定义、事件触发、事件转化、事件合并、事件排队、事件分派、事件处理、事 件连带等等。
其实,到目前为止,咱们尚未找到有关纯事件驱动编程的语言和相似的开发环境。全部关于事件驱动的资料都是基于GUI事件的。
属于事件驱动的编程语言有:VB、C#、Java(Java Swing的GUI)等。它们所涉及的事件绝大多数都是GUI事件。
过程化范式要求程序员用循序渐进的算法看待每一个问题。很显然,并非每一个问题都适合这种过程化的思惟方式。这也就致使了其它程序设计范式出现,包括咱们如今介绍的面向对象的程序设计范式。
面向对象的程序设计模式已经出现二十多年,通过这些年的发展,它的设计思想和设计模式已经稳定的进入编程语言的主流。来自TIOBE Programming Community2010年11月份编程语言排名的前三名Java、C、C++中,Java和C++都是面向对象的编程语言。
面向对象的程序设计包括了三个基本概念:封装性、继承性、多态性。面向对象的程序语言经过类、方法、对象和消息传递,来支持面向对象的程序设计范式。
1. 对象
世间万事万物都是对象。
面向对象的程序设计的抽象机制是将待解问题抽象为面向对象的程序中的对象。利用封装使每一个对象都拥有个体的身份。程序即是成堆的对象,彼此经过消息的传递,请求其它对象 进行工做。
2. 类
每一个对象都是其类中的一个实体。
物以类聚——就是说明:类是类似对象的集合。类中的对象能够接受相同的消息。换句话说:类包含和描述了“具备共同特性(数据元素)和共同行为(功能)”的一组对象。
好比:苹果、梨、橘子等等对象都属于水果类。
3. 封装
封装(有时也被称为信息隐藏)就是把数据和行为结合在一个包中,并对对象的使用者隐藏数据的实现过程。信息隐藏是面向对象编程的基本原则,而封装是实现这一原则的一种方 式。
封装使对象呈现出“黑盒子”特性,这是对象再利用和实现可靠性的关键步骤。
4. 接口
每一个对象都有接口。接口不是类,而是对符合接口需求的类所做的一套规范。接口说明类应该作什么但不指定如何做的方法。一个类能够有一个或多个接口。
5. 方法
方法决定了某个对象究竟可以接受什么样的消息。面向对象的设计有时也会简单地概括为“将消息发送给对象”。
6. 继承
继承的思想就是容许在已存在类的基础上构建新的类。一个子类可以继承父类的全部成员,包括属性和方法。
继承的主要做用:经过实现继承完成代码重用;经过接口继承完成代码被重用。继承是一种规范的技巧,而不是一种实现的技巧。
7. 多态
多态提供了“接口与实现分离”。多态不但能改善程序的组织架构及可读性,更利于开发出“可扩充”的程序。
继承是多态的基础。多态是继承的目的。
合理的运用基于类继承的多态、基于接口继承的多态和基于模版的多态,能加强程序的简洁性、灵活性、可维护性、可重用性和可扩展性。
面向对象技术一方面借鉴了哲学、心理学、生物学的思考方式,另外一方面,它是创建在其余编程技术之上的,是之前的编程思想的天然产物。
若是说结构化软件设计是将函数式编程技术应用到命令式语言中进行程序设计,面向对象编程不过是将函数式模型应用到命令式程序中的另外一途径,此时,模块进步为对象,过程龟缩到class的成员方法中。OOP的不少技术——抽象数据类型、信息隐藏、接口与实现分离、对象生成功能、消息传递机制等等,不少东西就是结构化软件设计所拥有的、或者在其余编程语言中单独出现。但只有在面向对象语言中,他们才共同出现,以一种独特的合做方式互相协做、互相补充。
编程范式 = 语感
知识的学习有几种方式:一种靠记忆,一种靠练习,一种靠培养。就拿英语学习来讲吧,学单词,单靠记忆便可;学句型、语法,光记忆是不够的,需要勤加练习方可熟能生巧;而要讲出地道的英语,光记忆和练习是远远不够的。从小学到大学,甚至博士毕业,除了英语类专业的学生外,大多数人英语练了一二十年,水平如何?不客气但很客观地说:一个字,烂。
缘由只有一个,那就是国内的英语教学方式严重失策。教学老是围绕单词、词组、句型、语法转,缺少对语感的重视和培养,致使学生只会‘中式英语’。一样道理,一个惯用C语言编程的人也许很快就能写一些C++程序,但若是他只注重C++的语法而不注重培养OOP 的语感,那么写出的程序必定是‘C 式C++’。与其如此,倒不如直接用C 呢。”
一句话:学习编程范式能加强编程语言的语感。
语感是一我的对语言的敏锐感知力,反映了他在语言方面的总体上的直觉把握能力。语感强者,能听弦外之音,能说双关之语,能读隽永之做,能写晓畅之文。这是一种综合的素质和修养,其重要性是不言而喻的。那么如何培养语感呢?普通的学习和训练固不可少,但若是忽视语言背后的文化背景和思惟方式,终究只是缘木求鱼。编程范式正体现了编程的思惟方式,于是是培养编程语言的语感的关键。
语感有了,那些设计模式、框架,甚至架构,等看似神秘高深的东西,也会天然而然地来了。
使用yield实现协程操做例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import
time
import
queue
def
consumer(name):
print
(
"--->starting eating baozi..."
)
while
True
:
new_baozi
=
yield
print
(
"[%s] is eating baozi %s"
%
(name,new_baozi))
#time.sleep(1)
def
producer():
r
=
con.__next__()
r
=
con2.__next__()
n
=
0
while
n <
5
:
n
+
=
1
con.send(n)
con2.send(n)
print
(
"\033[32;1m[producer]\033[0m is making baozi %s"
%
n )
if
__name__
=
=
'__main__'
:
con
=
consumer(
"c1"
)
con2
=
consumer(
"c2"
)
p
=
producer()
|
符合什么条件就能称之为协程:
基于上面这4点定义,咱们刚才用yield实现的程并不能算是合格的线程.
greenlet是一个用C实现的协程模块,相比与python自带的yield,它能够使你在任意函数之间随意切换,而不需把这个函数先声明为generator
greelet指的是使用一个任务调度器和一些生成器或者协程实现协做式用户空间多线程的一种伪并发机制,即所谓的微线程。
greelet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操做进行恢复为止。能够使用一个调度器循环在一组生成器函数之间协做多个任务。
网络框架的几种基本的网络I/O模型:
阻塞式单线程:这是最基本的I/O模型,只有在处理完一个请求以后才会处理下一个请求。它的缺点是效能差,若是有请求阻塞住,会让服务没法继续接受请求。可是这种模型编写代码相对简单,在应对访问量不大的状况时是很是适合的。
阻塞式多线程:针对于单线程接受请求量有限的缺点,一个很天然的想法就是给每个请求开一个线程去处理。这样作的好处是可以接受更多的请求,缺点是在线程产生到必定数量以后,进程之间须要大量进行切换上下文的操做,会占用CPU大量的时间,不过这样处理的话编写代码的难道稍高于单进程的状况。
非阻塞式事件驱动:为了解决多线程的问题,有一种作法是利用一个循环来检查是否有网络IO的事件发生,以便决定如何来进行处理(reactor设计模式)。这样的作的好处是进一步下降了CPU的资源消耗。缺点是这样作会让程序难以编写,由于请求接受后的处理过程由reactor来决定,使得程序的执行流程难以把握。当接受到一个请求后若是涉及到阻塞的操做,这个请求的处理就会停下来去接受另外一个请求,程序执行的流程不会像线性程序那样直观。twisted框架就是应用这种IO模型的典型例子。
非阻塞式Coroutine(协程):这个模式是为了解决事件驱动模型执行流程不直观的问题,它在本质上也是事件驱动的,加入了Coroutine的概念。
与线程/进程的区别
线程是抢占式的调度,多个线程并行执行,抢占共同的系统资源;而微线程是协同式的调度。
其实greenlet不是一种真正的并发机制,而是在同一线程内,在不一样函数的执行代码块之间切换,实施“你运行一会、我运行一会”,而且在进行切换时必须指定什么时候切换以及切换到哪。greenlet的接口是比较简单易用的,可是使用greenlet时的思考方式与其余并发方案存在必定区别:
1. 线程/进程模型在大逻辑上一般从并发角度开始考虑,把可以并行处理的而且值得并行处理的任务分离出来,在不一样的线程/进程下运行,而后考虑分离过程可能形成哪些互斥、冲突问题,将互斥的资源加锁保护来保证并发处理的正确性。
2. greenlet则是要求从避免阻塞的角度来进行开发,当出现阻塞时,就显式切换到另外一段没有被阻塞的代码段执行,直到原先的阻塞情况消失之后,再人工切换回原来的代码段继续处理。所以,greenlet本质是一种合理安排了的 串行 。
3. greenlet本质是串行,所以在没有进行显式切换时,代码的其余部分是没法被执行到的,若是要避免代码长时间占用运算资源形成程序假死,那么仍是要将greenlet与线程/进程机制结合使用(每一个线程、进程下均可以创建多个greenlet,可是跨线程/进程时greenlet之间没法切换或通信)。
使用
一个 “greenlet” 是一个很小的独立微线程。能够把它想像成一个堆栈帧,栈底是初始调用,而栈顶是当前greenlet的暂停位置。你使用greenlet建立一堆这样的堆栈,而后在他们之间跳转执行。跳转不是绝对的:一个greenlet必须选择跳转到选择好的另外一个greenlet,这会让前一个挂起,然后一个恢复。两 个greenlet之间的跳转称为 切换(switch) 。
当你建立一个greenlet,它获得一个初始化过的空堆栈;当你第一次切换到它,他会启动指定的函数,而后切换跳出greenlet。当最终栈底 函数结束时,greenlet的堆栈又编程空的了,而greenlet也就死掉了。greenlet也会由于一个未捕捉的异常死掉。
示例:来自官方文档示例
1
2
3
4
5
6
7
8
9
10
11
12
|
from
greenlet
import
greenlet
def
test1():
print
12
gr2.switch()
print
34
def
test2():
print
56
gr1.switch()
print
78
gr1
=
greenlet(test1)
gr2
=
greenlet(test2)
gr1.switch()
|
最后一行跳转到 test1() ,它打印12,而后跳转到 test2() ,打印56,而后跳转回 test1() ,打印34,而后 test1() 就结束,gr1死掉。这时执行会回到原来的 gr1.switch() 调用。注意,78是不会被打印的,由于gr1已死,不会再切换。
基于greenlet的框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# -*- coding:utf-8 -*-
from
greenlet
import
greenlet
def
test1():
print
(
12
)
gr2.switch()
print
(
34
)
gr2.switch()
def
test2():
print
(
56
)
gr1.switch()
print
(
78
)
gr1
=
greenlet(test1)
gr2
=
greenlet(test2)
gr1.switch()
|
Gevent 是一个第三方库,能够轻松经过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet所有运行在主程序操做系统进程的内部,但它们被协做式地调度。
结果:
gevent是一个基于协程(coroutine)的Python网络函数库,经过使用greenlet提供了一个在libev事件循环顶部的高级别并发API。
主要特性有如下几点:
基于libev的快速事件循环,Linux上面的是epoll机制
基于greenlet的轻量级执行单元
API复用了Python标准库里的内容
支持SSL的协做式sockets
可经过线程池或c-ares实现DNS查询
经过monkey patching功能来使得第三方模块变成协做式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import
gevent
def
func1():
print
(
'\033[31;1mTom在跟Jack搞...\033[0m'
)
gevent.sleep(
2
)
print
(
'\033[31;1mTom又回去跟继续跟Jack搞...\033[0m'
)
def
func2():
print
(
'\033[32;1mTom切换到了跟Sunny搞...\033[0m'
)
gevent.sleep(
1
)
print
(
'\033[32;1mTom搞完了Jack,回来继续跟Sunny搞...\033[0m'
)
gevent.joinall([
gevent.spawn(func1),
gevent.spawn(func2),
#gevent.spawn(func3),
])
|
结果:
1
2
3
4
5
6
7
8
|
Tom在跟Jack搞...
Tom切换到了跟Sunny搞...
Tom搞完了Jack,回来继续跟Sunny搞...
Tom又回去跟继续跟Jack搞...
Tom在跟Jack搞...
Tom切换到了跟Sunny搞...
Tom搞完了Jack,回来继续跟Sunny搞...
Tom又回去跟继续跟Jack搞...
|
经过gevent实现单线程下的多socket并发
server side
client side
简单的使用协程写一个爬虫:
串行
并行:
eventlet
eventlet 是基于 greenlet 实现的面向网络应用的并发处理框架,提供“线程”池、队列等与其余 Python 线程、进程模型很是类似的 api,而且提供了对 Python 发行版自带库及其余模块的超轻量并发适应性调整方法,比直接使用 greenlet 要方便得多。
其基本原理是调整 Python 的 socket 调用,当发生阻塞时则切换到其余 greenlet 执行,这样来保证资源的有效利用。须要注意的是:
eventlet 提供的函数只能对 Python 代码中的 socket 调用进行处理,而不能对模块的 C 语言部分的 socket 调用进行修改。对后者这类模块,仍然须要把调用模块的代码封装在 Python 标准线程调用中,以后利用 eventlet 提供的适配器实现 eventlet 与标准线程之间的协做。
虽然 eventlet 把 api 封装成了很是相似标准线程库的形式,但二者的实际并发执行流程仍然有明显区别。在没有出现 I/O 阻塞时,除非显式声明,不然当前正在执行的 eventlet 永远不会把 cpu 交给其余的 eventlet,而标准线程则是不管是否出现阻塞,老是由全部线程一块儿争夺运行资源。全部 eventlet 对 I/O 阻塞无关的大运算量耗时操做基本没有什么帮助。
关于Linux的epoll机制:
epoll是Linux内核为处理大批量文件描述符而做了改进的poll,是Linux下多路复用IO接口select/poll的加强版本,它能显著提升程序在大量并发链接中只有少许活跃的状况下的系统CPU利用率。epoll的优势:
支持一个进程打开大数目的socket描述符。select的一个进程所打开的FD由FD_SETSIZE的设置来限定,而epoll没有这个限制,它所支持的FD上限是最大可打开文件的数目,远大于2048。
IO效率不随FD数目增长而线性降低:因为epoll只会对“活跃”的socket进行操做,因而,只有"活跃"的socket才会主动去调用 callback函数,其余idle状态的socket则不会。
使用mmap加速内核与用户空间的消息传递。epoll是经过内核于用户空间mmap同一块内存实现的。
内核微调。
libev机制
提供了指定文件描述符事件发生时调用回调函数的机制。libev是一个事件循环器:向libev注册感兴趣的事件,好比socket可读事件,libev会对所注册的事件的源进行管理,并在事件发生时触发相应的程序。
官方文档中的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
>>>
import
gevent
>>>
from
gevent
import
socket
>>> urls
=
[
'www.google.com.hk'
,
'www.example.com'
,
'www.python.org'
]
>>> jobs
=
[gevent.spawn(socket.gethostbyname, url)
for
url
in
urls]
>>> gevent.joinall(jobs, timeout
=
2
)
>>> [job.value
for
job
in
jobs]
[
'74.125.128.199'
,
'208.77.188.166'
,
'82.94.164.162'
]
|
注解:gevent.spawn()方法spawn一些jobs,而后经过gevent.joinall将jobs加入到微线程执行队列中等待其完成,设置超时为2秒。执行后的结果经过检查gevent.Greenlet.value值来收集。gevent.socket.gethostbyname()函数与标准的socket.gethotbyname()有相同的接口,但它不会阻塞整个解释器,所以会使得其余的greenlets跟随着无阻的请求而执行。
Monket patching
Python的运行环境容许咱们在运行时修改大部分的对象,包括模块、类甚至函数。虽然这样作会产生“隐式的反作用”,并且出现问题很难调试,但在须要修改Python自己的基础行为时,Monkey patching就派上用场了。Monkey patching可以使得gevent修改标准库里面大部分的阻塞式系统调用,包括socket,ssl,threading和select等模块,而变成协做式运行。
>>> from gevent import monkey ;
>>> monkey . patch_socket ()
>>> import urllib2
经过monkey.patch_socket()方法,urllib2模块能够使用在多微线程环境,达到与gevent共同工做的目的。
事件循环
不像其余网络库,gevent和eventlet相似, 在一个greenlet中隐式开始事件循环。没有必须调用run()或dispatch()的反应器(reactor),在twisted中是有 reactor的。当gevent的API函数想阻塞时,它得到Hub实例(执行时间循环的greenlet),并切换过去。若是没有集线器实例则会动态 建立。
libev提供的事件循环默认使用系统最快轮询机制,设置LIBEV_FLAGS环境变量可指定轮询机制。LIBEV_FLAGS=1为select, LIBEV_FLAGS = 2为poll, LIBEV_FLAGS = 4为epoll,LIBEV_FLAGS = 8为kqueue。
Libev的API位于gevent.core下。注意libev API的回调在Hub的greenlet运行,所以使用同步greenlet的API。能够使用spawn()和Event.set()等异步API。
同步与异步的性能区别
上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn
。 初始化的greenlet列表存放在数组threads
中,此数组被传给gevent.joinall
函数,后者阻塞当前流程,并执行全部给定的greenlet。执行流程只会在 全部greenlet执行完后才会继续向下走。
遇到IO阻塞时会自动切换任务
经过gevent实现单线程下的多socket并发
server side
client side
并发100个sock链接
全部的计算机程序均可以大体分为两类:脚本型(单次运行)和连续运行型(直到用户主动退出)。
脚本型
脚本型的程序包括最先的批处理文件以及使用Python作交易策略回测等等,这类程序的特色是在用户启动后会按照编程时设计好的步骤一步步运行,全部步骤运行完后自动退出。
连续运行型
连续运行型的程序包含了操做系统和绝大部分咱们平常使用的软件等等,这类程序启动后会处于一个无限循环中连续运行,直到用户主动退出时才会结束。
咱们要开发的交易系统就是属于连续运行型程序,而这种程序根据其计算逻辑的运行机制不一样,又能够粗略的分为时间驱动和事件驱动两种。
时间驱动
时间驱动的程序逻辑相对容易设计,简单来讲就是让电脑每隔一段时间自动作一些事情。这个事情自己能够很复杂、包括不少步骤,但这些步骤都是线性的,按照顺序一步步执行下来。
如下代码展现了一个很是简单的时间驱动的Python程序。
1
2
3
4
5
6
7
8
|
from
time
import
sleep
def
demo():
print
u
'时间驱动的程序每隔1秒运行demo函数'
while
1
:
demo()
sleep(
1.0
)
|
时间驱动的程序本质上就是每隔一段时间固定运行一次脚本(上面代码中的demo函数)。尽管脚本自身能够很长、包含很是多的步骤,可是咱们能够看出这种程序的运行机制相对比较简单、容易理解。
举一些量化交易相关的例子:
对速度要求较高的量化交易方面(日内CTA策略、高频策略等等),时间驱动的程序会存在一个很是大的缺点:对数据信息在反应操做上的处理延时。例子2中,在每次逻辑脚本运行完等待的那1秒钟里,程序对于接收到的新数据信息(行情、成交推送等等)是不会作出任何反应的,只有在等待时间结束后脚本再次运行时才会进行相关的计算处理。而处理延时在量化交易中的直接后果就是money:市价单滑点、限价单错过本可成交的价格。
时间驱动的程序在量化交易方面还存在一些其余的缺点:如浪费CPU的计算资源、实现异步逻辑复杂度高等等。
事件驱动
与时间驱动对应的就是事件驱动的程序:当某个新的事件被推送到程序中时(如API推送新的行情、成交),程序当即调用和这个事件相对应的处理函数进行相关的操做。
上面例子2的事件驱动版:交易程序对股指TICK数据进行监听,当没有新的行情过来时,程序保持监听状态不进行任何操做;当收到新的数据时,数据处理函数当即更新K线和其余技术指标,并检查是否知足趋势策略的下单条件执行下单。
对于简单的程序,咱们能够采用上面测试代码中的方案,直接在API的回调函数中写入相应的逻辑。但随着程序复杂度的增长,这种方案会变得愈来愈不可行。假设咱们有一个带有图形界面的量化交易系统,系统在某一时刻接收到了API推送的股指期货行情数据,针对这个数据系统须要进行以下处理:
此时将上面全部的操做都写到一个回调函数中无疑变成了很是差的方案,代码过长容易出错不说,可扩展性也差,每添加一个策略或者功能则又须要修改以前的源代码(有经验的读者会知道,常常修改生产代码是一种很是危险的运营管理方法)。
为了解决这种状况,咱们须要用到事件驱动引擎来管理不一样事件的事件监听函数并执行全部和事件驱动相关的操做。
事件驱动模式能够进一步抽象理解为由事件源,事件对象,以及事件监听器三元素构成,能完成监听器监听事件源、事件源发送事件,监听器收到事件后调用响应函数的动做。
事件驱动主要包含如下元素和操做函数:
元素
1.事件源
2.事件监听器
3.事件对象
操做函数
4.监听动做
5.发送事件
6.调用监听器响应函数
了解清楚了事件驱动的工做原理后,读者能够试着用本身熟悉的编程语言实现,编程主要实现下面的内容,笔者后续给python实现:
用户根据实际业务逻辑定义
事件源 EventSources
监听器 Listeners
事件管理者 EventManager
成员
1.响应函数队列 Handlers
2.事件对象 Event
3.事件对象列表 EventQueue
操做函数
4.监听动做 AddEventListener
5.发送事件 SendEvent
6.调用响应函数 EventProcess
在实际的软件开发过程当中,你会常常看到事件驱动的影子,几乎全部的GUI界面都采用事件驱动编程模型,不少服务器网络模型的消息处理也会采用,甚至复杂点的数据库业务处理也会用这种模型,由于这种模型解耦事件发送者和接收者之间的联系,事件可动态增长减小接收者,业务逻辑越复杂,越能体现它的优点。
论事件驱动模型
在UI编程中,,经常要对鼠标点击进行相应,首先如何得到鼠标点击呢?
方式一:建立一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有如下几个缺点:
方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如不少UI平台都会提供onClick()事件,这个事件就表明鼠标按下事件。事件驱动模型大致思路以下:
最初的问题:怎么肯定IO操做完了切回去呢?经过回调函数
在单线程同步模型中,任务按照顺序执行。若是某个任务由于I/O而阻塞,其余全部的任务都必须等待,直到它完成以后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。若是任务之间并无互相依赖的关系,但仍然须要互相等待的话这就使得程序没必要要的下降了运行速度。
在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操做系统来管理,在多处理器系统上能够并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其余线程得以继续执行。与完成相似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,由于这类程序不得不经过线程同步机制如锁、可重入函数、线程局部存储或者其余机制来处理线程安全问题,若是实现不当就会致使出现微妙且使人痛不欲生的bug。
在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其余昂贵的操做时,注册一个回调到事件循环中,而后当I/O操做完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询全部的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽量的得以执行而不须要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,由于程序员不须要关心线程安全问题。
当咱们面对以下的环境时,事件驱动模型一般是一个好的选择:
当应用程序须要在任务间共享可变的数据时,这也是一个不错的选择,由于这里不须要采用同步处理。
网络应用程序一般都有上述这些特色,这使得它们可以很好的契合事件驱动编程模型。
事件驱动编程是指程序的执行流程取决于事件的编程风格,事件由事件处理程序或者事件回调函数进行处理。当某些重要的事件发生时-- 例如数据库查询结果可用或者用户单击了某个按钮,就会调用事件回调函数。
事件驱动编程风格和事件循环相伴相生,事件循环是一个处于不间断循环中的结构,该结构主要具备两项功能-- 事件检测和事件触发处理,在每一轮循环中,它都必须检测发生了什么事件。当事件发生时,事件循环还要决定调用哪一个回调函数。
事件循环只是在一个进程中运行的单个线程,这意味着当事件发生时,能够不用中断就运行事件处理程序,这样作有如下两个特色:
在任一给定时刻,最多运行一个事件处理程序。
事件处理程序能够不间断地运行直到结束。
这使得程序员能放宽同步要求,而且没必要担忧执行并发线程会改变共享内存的状态。
众所周知的秘密
在至关一段时间内,系统编程领域已经知道事件驱动编程是建立处理众多并发链接的服务的最佳方法。众所周知,因为不用保存不少上下文,所以节省了大量内存;又由于也没有那么多上下文切换,又节省了大量执行时间。
大名鼎鼎的Nginx使用了多进程模型,主进程启动时初始化,bind,监听一组sockets,而后fork一堆child processes(workers),workers共享socket descriptor。workers竞争accept_mutex,获胜的worker经过IO multiplex(select/poll/epoll/kqueue/...)来处理成千上万的并发请求。为了得到高性能,Nginx还大量使用了异步,事件驱动,non-blocking IO等技术。"What resulted is amodular, event-driven, asynchronous, single-threaded, non-blockingarchitecture which became the foundation of nginx code."
Nginx 架构
对比着看一下Apache的两种经常使用运行模式,详见 Apache Modules
1. Apache MPM prefork模式
主进程经过进程池维护必定数量(可配置)的worker进程,每一个worker进程负责一个connection。worker进程之间经过竞争mpm-accept mutex实现并发和连接处理隔离。 因为进程内存开销和切换开销,该模式相对来讲是比较低效的并发。
2. Apache MPM worker模式
因为进程开销较大,MPM worker模式作了改进,处理每一个connection的实体改成thread。主进程启动可配数量的子进程,每一个进程启动可配数量的server threads和listen thread。listen threads经过竞争mpm-accept mutex获取到新进的connection request经过queue传递给本身进程所在的server threads处理。因为调度的实体变成了开销较小的thread,worker模式相对prefork具备更好的并发性能。
小结两种webserver,能够发现Nginx使用了更高效的编程模型,worker进程通常跟CPU的core数量至关,每一个worker驻留在一个core上,合理编程能够作到最小程度的进程切换,并且内存的使用也比较经济,基本上没有浪费在进程状态的存储上。而Apache的模式是每一个connection对应一个进程/线程,进程/线程间的切换开销,大量进程/线程的内存开销,cache miss的几率增大,都限制了系统所能支持的并发数。
如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:
1. 保存处理机上下文,包括程序计数器和其余寄存器。
2. 更新PCB信息。
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4. 选择另外一个进程执行,并更新其PCB。
5. 更新内存管理的数据结构。
6. 恢复处理机上下文。
注:总而言之就是很耗资源,具体的能够参考这篇文章:进程切换
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的
。
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。
缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。
因为IO的处理速度要远远低于CPU的速度,运行在CPU上的程序不得不考虑IO在准备暑假的过程当中该干点什么,让出CPU给别人仍是本身去干点别的有意义的事情,这就涉及到了采用什么样的IO策略。通常IO策略的选用跟进程线程编程模型要同时考虑,二者是有联系的。
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO能够理解为对流的操做。刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。因此说,当一个read操做发生时,它会经历两个阶段:
第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于socket流而言,
第一步:一般涉及等待网络上的数据分组到达,而后被复制到内核的某个缓冲区。
第二步:把数据从内核缓冲区复制到应用进程缓冲区。
网络应用须要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。
IO介绍
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执行的结果。比如你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现作,须要等5分钟,因而你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。
你说“来个汉堡”,服务员告诉你,汉堡须要等5分钟,你能够先去逛商场,等作好了,咱们再通知你,这样你能够马上去干别的事情(逛商场),这是异步IO。
很明显,使用异步IO来编写程序性能会远远高于同步IO,可是异步IO的缺点是编程模型复杂。想一想看,你得知道何时通知你“汉堡作好了”,而通知你的方法也各不相同。若是是服务员跑过来找到你,这是回调模式,若是服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO。
操做IO的能力都是由操做系统提供的,每一种编程语言都会把操做系统提供的低级C接口封装起来方便使用,Python也不例外。咱们后面会详细讨论Python的IO编程接口。
接触网络编程,咱们时常会与各类与IO相关的概念打交道:同步(Synchronous)、异步(ASynchronous)、阻塞(blocking)和非阻塞(non-blocking)。
同步与异步的主要区别就在于:会不会致使请求进程(或线程)阻塞。同步会使请求进程(或线程)阻塞而异步不会。
linux下有五种常见的IO模型,其中只有一种异步模型,其他皆为同步模型。如图:
同步:
所谓同步,就是在发出一个功能调用时,在没有获得结果以前,该调用就不返回。也就是必须一件一件事作,等前一件作完了才能作下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能马上获得结果。实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求经过事件触发->服务器处理(这是浏览器仍然能够做其余事情)->处理完毕
这就是同步和异步。举个简单的例子,假若有一个任务包括两个子任务A和B,对于同步来讲,当A在执行的过程当中,B只有等待,直至A执行完毕,B才能执行;而对于异步就是A和B能够并发地执行,B没必要等待A执行完毕以后再执行,这样就不会因为A的执行致使整个任务的暂时等待。
阻塞
阻塞调用是指调用结果返回以前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在获得结果以后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不一样的。对于同步调用来讲,不少时候当前线程仍是激活的,只是从逻辑上当前函数没有返回而已。 例如,咱们在socket中调用recv函数,若是缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各类各样的消息。
非阻塞
非阻塞和阻塞的概念相对应,指在不能马上获得结果以前,该函数不会阻塞当前线程,而会马上返回。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是否是阻塞调用有很强的相关性,可是并非一一对应的。阻塞对象上能够有非阻塞的调用方式,咱们能够经过必定的API去轮询状 态,在适当的时候调用阻塞函数,就能够避免阻塞。而对于非阻塞对象,调用特殊的函数也能够进入阻塞调用。函数select就是这样的一个例子。
这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操做时,若是条件不知足,是会一直等待仍是返回一个标志信息。
1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
2. 异步,就是我调用一个功能,不须要知道该功能结果,该功能有结果后通知我(回调通知)
3. 阻塞, 就是调用我(函数),我(函数)没有接收完数据或者没有获得结果以前,我不会返回。
4. 非阻塞, 就是调用我(函数),我(函数)当即返回,经过select通知调用者
同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否当即返回!
对于举个简单c/s 模式:
同步和异步,阻塞和非阻塞,有些混用,其实它们彻底不是一回事,并且它们修饰的对象也不相同。
阻塞和非阻塞是指当进程访问的数据若是还没有就绪,进程是否须要等待,简单说这至关于函数内部的实现区别,也就是未就绪时是直接返回仍是等待就绪;
而同步和异步是指访问数据的机制,同步通常指主动请求并等待I/O操做完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后即可以继续处理其它任务,随后等待I/O,操做完毕的通知,这能够使进程在数据读写时也不阻塞。(等待"通知")
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步,只有最后一种才是异步IO。
注:因为signal driven IO在实际中并不经常使用,因此我这只说起剩下的四种IO Model。
在深刻介绍Linux IO各类模型以前,让咱们先来探索一下基本 Linux IO 模型的简单矩阵。以下图所示:
每一个 IO 模型都有本身的使用模式,它们对于特定的应用程序都有本身的优势。本节将简要对其一一进行介绍。常见的IO模型有阻塞、非阻塞、IO多路复用,异步。
阻塞式I/O;
非阻塞式I/O;
I/O复用;
信号驱动式I/O;
异步I/O;
一个输入操做一般包括两个不一样的阶段:
1) 等待数据准备好;
2) 从内核向进程复制数据;
对于一个套接字上的输入操做,第一步一般涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
网络IO操做实际过程涉及到内核和调用这个IO操做的进程。以read为例,read的具体操做分为如下两个部分:
(1)内核等待数据可读
(2)将内核读到的数据拷贝到进程
简介:进程会一直阻塞,直到数据拷贝完成
应用程序调用一个IO函数,致使应用程序阻塞,等待数据准备好。 若是数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
最流行的I/O模型是阻塞式I/O(blocking I/O)模型,默认状况下,全部套接字都是阻塞的。以数据报套接字做为例子,咱们有如图6-1所示的情形。
阻塞I/O模型图:在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。
同步阻塞IO
当调用recv()函数时,系统首先查是否有准备好的数据。若是数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,而后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。
当使用socket()函数和WSASocket()函数建立套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能当即完成时,线程处于等待状态,直到操做完成。
并非全部Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会当即返回。将可能阻塞套接字的Windows Sockets API调用分为如下四种:
1.输入操做: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。若是此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。
2.输出操做: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。若是套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。
3.接受链接:accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的链接请求。若是此时没有链接请求,线程就会进入睡眠状态。
4.外出链接:connect()和WSAConnect()函数。对于TCP链接,客户端以阻塞套接字为参数,调用该函数向服务器发起链接。该函数在收到服务器的应答前,不会返回。这意味着TCP链接总会等待至少到服务器的一次往返时间。
使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当但愿可以当即发送和接收数据,且处理的套接字数量比较少的状况下,使用阻塞模式来开发网络程序比较合适。
阻塞模式套接字的不足表现为,在大量创建好的套接字线程之间进行通讯时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每一个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当但愿同时处理大量套接字时,将无从下手,其扩展性不好
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操做非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误.
咱们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操做没法完成时,不要将进程睡眠,而是返回一个错误。这样咱们的I/O操做函数将不断的测试数据是否已经准备好,若是没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程当中,会大量的占用CPU的时间。
把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数当即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字屡次调用recv()函数的过程。前三次调用recv()函数时,内核数据尚未准备好。所以,该函数当即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。
前三次调用recvfrom时没有数据可返回,所以内核转而当即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,因而recvfrom成功返回。咱们接着处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,咱们称之为轮询(polling)。应用进程只需轮询内核,以查看某个操做是否就绪。这么作每每耗费大量CPU时间。
当使用socket()函数和WSASocket()函数建立套接字时,默认都是阻塞的。在建立套接字以后,经过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().
套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会当即返回。大多数状况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操做在调用期间内没有时间完成。一般,应用程序须要重复调用该函数,直到得到成功返回代码。
须要说明的是并不是全部的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。固然,在调用WSAStartup()函数时更不会返回该错误代码,由于该函数是应用程序第一调用的函数,固然不会返回这样的错误代码。
要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数以外,还能够使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。
因为使用非阻塞套接字在调用函数时,会常常返回WSAEWOULDBLOCK错误。因此在任什么时候候,都应仔细检查返回代码并做好对“失败”的准备。应用程序接二连三地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种作法很浪费系统资源。
要完成这样的操做,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。一样,这种方法也很差。由于该作法对系统形成的开销是很大的,而且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的作法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。
非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,须要编写更多的代码,以便在每一个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。所以,非阻塞套接字便显得有些难于使用。
可是,非阻塞套接字在控制创建的多个链接,在数据的收发量不均,时间不定时,明显具备优点。这种套接字在使用上存在必定难度,但只要排除了这些困难,它在功能上仍是很是强大的。一般状况下,可考虑使用套接字的“I/O模型”,它有助于应用程序经过异步方式,同时对一个或多个套接字的通讯加以管理。
简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并无什么优越性;关键是能实现同时对多个IO端口进行监听;
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,可是和阻塞I/O所不一样的的,这两个函数能够同时阻塞多个I/O操做。并且能够同时对多个读操做,多个写操做的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操做函数。
咱们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,咱们调用recvfrom把所读数据报复制到应用进程缓冲区。
比较图6-3和图6-1,I/O复用并不显得有什么优点,事实上因为使用select须要两个而不是单个系统调用,I/O复用还稍有劣势。使用select的优点在于咱们能够等待多个描述符就绪。
与I/O复用密切相关的另外一种I/O模型是在多线程中使用阻塞式I/O(咱们常常这么干)。这种模型与上述模型极为类似,但它并无使用select阻塞在多个文件描述符上,而是使用多个线程(每一个文件描述符一个线程),这样每一个线程均可以自由的调用recvfrom之类的阻塞式I/O系统调用了。
简介:两次调用,两次返回;
咱们也能够用信号,让内核在描述符就绪时发送SIGIO信号通知咱们。咱们称这种模型为信号驱动式I/O(signal-driven I/O),图6-4是它的概要展现。
首先咱们容许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,能够在信号处理函数中调用I/O操做函数处理数据。
咱们首先开启套接字的信号驱动式I/O功能,并经过sigaction系统调用安装一个信号处理函数。改系统调用将当即返回,咱们的进程继续工做,也就是说他没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。咱们随后就能够在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理,也能够当即通知主循环,让它读取数据报。
不管如何处理SIGIO信号,这种模型的优点在于等待数据报到达期间进程不被阻塞。主循环能够继续执行,只要等到来自信号处理函数的通知:既能够是数据已准备好被处理,也能够是数据报已准备好被读取。
简介:数据拷贝的时候进程无需阻塞。
异步I/O(asynchronous I/O)由POSIX规范定义。演变成当前POSIX规范的各类早起标准所定义的实时函数中存在的差别已经取得一致。通常地说,这些函数的工做机制是:告知内核启动某个操做,并让内核在整个操做(包括将数据从内核复制到咱们本身的缓冲区)完成后通知咱们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知咱们什么时候能够启动一个I/O操做,而异步I/O模型是由内核通知咱们I/O操做什么时候完成。图6-5给出了一个例子。
当一个异步过程调用发出后,调用者不能马上获得结果。实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者的输入输出操做
异步非阻塞IO
对比同步非阻塞IO,异步非阻塞IO也有个名字--Proactor。这种策略是真正的异步,使用注册callback/hook函数来实现异步。程序注册本身感兴趣的socket 事件时,同时将处理各类事件的handler也就是对应的函数也注册给内核,不会有任何阻塞式调用。事件发生后内核之间调用对应的handler完成处理。这里暂且理解为内核作了event的调度和handler调用,具体究竟是异步IO库如何作的,如何跟内核通讯的,后续继续研究。
同步IO引发进程阻塞,直至IO操做完成。
异步IO不会引发进程阻塞。
IO复用是先经过select调用阻塞。
咱们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek相似),并告诉内核当整个操做完成时如何通知咱们。该系统调用当即返回,而且在等待I/O完成期间,咱们的进程不被阻塞。本例子中咱们假设要求内核在操做完成时产生某个信号。改信号直到数据已复制到应用进程缓冲区才产生,这一点不一样于信号驱动I/O模型。
图6-6对比了上述5中不一样的I/O模型。能够看出,前4中模型的主要区别在于第一阶段,由于他们的第二阶段是同样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不一样于其余4中模型。
POSIX把这两个术语定于以下:
同步I/O操做(sysnchronous I/O opetation)致使请求进程阻塞,直到I/O操做完成;
异步I/O操做(asynchronous I/O opetation)不致使请求进程阻塞。
根据上述定义,咱们的前4种模型----阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动I/O模型都是同步I/O模型,由于其中真正的I/O操做(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。
IO多路复用之select、poll、epoll详解
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用以下场合:
当客户处理多个描述符时(通常是交互式输入和网络套接口),必须使用I/O复用。
当一个客户同时处理多个套接口时,而这种状况是可能的,但不多出现。
若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。
若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。
若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程
,也没必要维护这些进程/线程,从而大大减少了系统的开销。
目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll
,I/O多路复用就是经过一种机制,一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做
。但select,pselect,poll,epoll本质上都是同步I/O
,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
对于IO多路复用机制不理解的同窗,能够先行参考《聊聊Linux 五种IO模型》,来了解Linux五种IO模型。
epoll跟select都能提供多路I/O复用的解决方案。在如今的Linux内核里有都可以支持,其中epoll是Linux所特有,而select则应该是POSIX所规定
,通常操做系统均有实现。
基本原理:
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,若是当即返回设为null便可),函数返回。当select函数返回后,能够经过遍历fdset,来找到就绪的描述符。
基本概念
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用以下场合:
(1)当客户处理多个描述字时(通常是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种状况是可能的,但不多出现。
(3)若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。
(4)若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。
(5)若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程,也没必要维护这些进程/线程,从而大大减少了系统的开销。
select的调用过程以下所示:
(1)使用copy_from_user从用户空间拷贝fd_set到内核空间
(2)注册回调函数__pollwait
(3)遍历全部fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据状况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工做就是把current(当前进程)挂到设备的等待队列中,不一样的设备有不一样的等待队列,对于tcp_poll来讲,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不表明进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操做是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)若是遍历完全部的fd,尚未返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若是超过必定的超时时间(schedule_timeout指定),仍是没人唤醒,则调用select的进程会从新被唤醒得到CPU,进而从新遍历fd,判断有没有就绪的fd。
(8)把fd_set从内核空间拷贝到用户空间。
基本流程,如图所示:
select目前几乎在全部的平台上支持,其良好跨平台支持也是它的一个优势
。select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制
,在Linux上通常为1024,能够经过修改宏定义甚至从新编译内核的方式提高这一限制
,可是这样也会形成效率的下降。
调用select的函数为r, w, e = select.select(rlist, wlist, xlist[, timeout])
,前三个参数都分别是三个列表,数组中的对象均为waitable object
:均是整数的文件描述符(file descriptor)或者一个拥有返回文件描述符方法fileno()
的对象;
rlist
: 等待读就绪的listwlist
: 等待写就绪的listerrlist
: 等待“异常”的list
select方法用来监视文件描述符,若是文件描述符发生变化,则获取该描述符。
二、当 rlist
序列中的描述符发生可读时(accetp和read),则获取发生变化的描述符并添加到 r
序列中
三、当 wlist
序列中含有描述符时,则将该序列中全部的描述符添加到 w
序列中
四、当 errlist
序列中的句柄发生错误时,则将该发生错误的句柄添加到 e
序列中
五、当 超时时间 未设置,则select会一直阻塞,直到监听的描述符发生变化
当 超时时间 =
1
时,那么若是监听的句柄均无任何变化,则select会阻塞
1
秒,以后返回三个空列表,若是监听的描述符(fd)有变化,则直接执行。
六、在list中能够接受Ptython的的file
对象(好比sys.stdin
,或者会被open()
和os.open()
返回的object),socket object将会返回socket.socket()
。也能够自定义类,只要有一个合适的fileno()
的方法(须要真实返回一个文件描述符,而不是一个随机的整数)。
select代码注释
select本质上是经过设置或者检查存放fd标志位的数据结构来进行下一步处理
。这样所带来的缺点是:
select最大的缺陷就是单个进程所打开的FD是有必定限制的,它由FD_SETSIZE设置,默认值是1024。
通常来讲这个数目和系统内存关系很大,
具体数目能够cat /proc/sys/fs/file-max察看
。32位机默认是1024个。64位机默认是2048.
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要经过遍历FD_SETSIZE个Socket来完成调度,无论哪一个Socket是活跃的,都遍历一遍。这会浪费不少CPU时间。
若是能给套接字注册某个回调函数,当他们活跃时,自动完成相关操做,那就避免了轮询
,这正是epoll与kqueue作的。
须要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
select的几大缺点:
(1)每次调用select,都须要把fd集合从用户态拷贝到内核态,这个开销在fd不少时会很大
(2)同时每次调用select都须要在内核遍历传递进来的全部fd,这个开销在fd不少时也很大
(3)select支持的文件描述符数量过小了,默认是1024
基本原理:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间
,而后查询每一个fd对应的设备状态,若是设备就绪则在设备等待队列中加入一项并继续遍历,若是遍历完全部fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了屡次无谓的遍历。
它没有最大链接数的限制,缘由是它是基于链表来存储的
,可是一样有一个缺点:
大量的fd的数组被总体复制于用户态和内核地址空间之间
,而无论这样的复制是否是有意义。
poll还有一个特色是“水平触发”
,若是报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
注意:
从上面看,select和poll都须要在返回后,
经过遍历文件描述符来获取已经就绪的socket
。事实上,同时链接的大量客户端在一时刻可能只有不多的处于就绪状态
,所以随着监视的描述符数量的增加,其效率也会线性降低。
select.poll()
,返回一个poll的对象,支持注册和注销文件描述符。
poll.register(fd[, eventmask])
注册一个文件描述符,注册后,能够经过poll()
方法来检查是否有对应的I/O事件发生。fd
能够是i 个整数,或者有返回整数的fileno()
方法对象。若是File对象实现了fileno(),也能够看成参数使用。
eventmask
是一个你想去检查的事件类型,它能够是常量POLLIN
, POLLPRI
和 POLLOUT
的组合。若是缺省,默认会去检查全部的3种事件类型。
事件常量 | 意义 |
---|---|
POLLIN | 有数据读取 |
POLLPRT | 有数据紧急读取 |
POLLOUT | 准备输出:输出不会阻塞 |
POLLERR | 某些错误状况出现 |
POLLHUP | 挂起 |
POLLNVAL | 无效请求:描述没法打开 |
poll.modify(fd, eventmask)
修改一个已经存在的fd,和poll.register(fd, eventmask)
有相同的做用。若是去尝试修改一个未经注册的fd,会引发一个errno
为ENOENT的IOError
。poll.unregister(fd)
从poll对象中注销一个fd。尝试去注销一个未经注册的fd,会引发KeyError
。poll.poll([timeout])
去检测已经注册了的文件描述符。会返回一个可能为空的list,list中包含着(fd, event)
这样的二元组。 fd
是文件描述符, event
是文件描述符对应的事件。若是返回的是一个空的list,则说明超时了且没有文件描述符有事件发生。timeout
的单位是milliseconds,若是设置了timeout
,系统将会等待对应的时间。若是timeout
缺省或者是None
,这个方法将会阻塞直到对应的poll对象有一个事件发生。poll代码
在linux2.6(准确来讲是2.5.44)由内核直接支持的方法。epoll解决了select和poll的缺点。
epoll同时支持水平触发和边缘触发:
epoll.poll()
会重复通知关注的event,直到与该event有关的全部数据都已被处理。(select, poll是水平触发, epoll默认水平触发)epoll.poll()
的程序必须处理全部和这个event相关的数据,随后的epoll.poll()
调用不会再有这个event的通知。select.epoll([sizehint=-1])
返回一个epoll对象。
eventmask
事件常量 | 意义 |
---|---|
EPOLLIN | 读就绪 |
EPOLLOUT | 写就绪 |
EPOLLPRI | 有数据紧急读取 |
EPOLLERR | assoc. fd有错误状况发生 |
EPOLLHUP | assoc. fd发生挂起 |
EPOLLRT | 设置边缘触发(ET)(默认的是水平触发) |
EPOLLONESHOT | 设置为 one-short 行为,一个事件(event)被拉出后,对应的fd在内部被禁用 |
EPOLLRDNORM | 和 EPOLLIN 相等 |
EPOLLRDBAND | 优先读取的数据带(data band) |
EPOLLWRNORM | 和 EPOLLOUT 相等 |
EPOLLWRBAND | 优先写的数据带(data band) |
EPOLLMSG | 忽视 |
epoll.close()
关闭epoll对象的文件描述符。epoll.fileno
返回control fd的文件描述符number。epoll.fromfd(fd)
用给予的fd来建立一个epoll对象。epoll.register(fd[, eventmask])
在epoll对象中注册一个文件描述符。(若是文件描述符已经存在,将会引发一个IOError
)epoll.modify(fd, eventmask)
修改一个已经注册的文件描述符。epoll.unregister(fd)
注销一个文件描述符。epoll.poll(timeout=-1[, maxevnets=-1])
等待事件,timeout(float)的单位是秒(second)。基本原理:
epoll支持水平触发和边缘触发,最大的特色在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,而且只会通知一次
。还有一个特色是,epoll使用“事件”的就绪通知方式
,经过epoll_ctl注册fd,一旦该fd就绪,内核就会采用相似callback的回调机制来激活该fd
,epoll_wait即可以收到通知。
epoll代码注释
epoll的优势:
没有最大并发链接的限制
,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
效率提高,不是轮询的方式,不会随着FD数目的增长效率降低
。只有活跃可用的FD才会调用callback函数;即Epoll最大的优势就在于它只管你“活跃”的链接,而跟链接总数无关
,所以在实际的网络环境中,Epoll的效率就会远远高于select和poll。
内存拷贝
,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减小复制开销
。
epoll对文件描述符的操做有两种模式:LT(level trigger)和ET(edge trigger)
。LT模式是默认模式,LT模式与ET模式的区别以下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序能够不当即处理该事件
。下次调用epoll_wait时,会再次响应应用程序并通知此事件。ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序必须当即处理该事件
。若是不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
LT模式
LT(level triggered)是缺省的工做方式,而且同时支持block和no-block socket
。在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你的
。
ET模式
ET(edge-triggered)是高速工做方式,只支持no-block socket
。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使了一个EWOULDBLOCK 错误)。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once)
。
ET模式在很大程度上减小了epoll事件被重复触发的次数,所以效率要比LT模式高
。epoll工做在ET模式的时候,必须使用非阻塞套接口
,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。
在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描
,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制
,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。(此处去掉了遍历文件描述符,而是经过监听回调的的机制。这正是epoll的魅力所在。
)
注意:
若是没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高不少,可是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。
支持一个进程所能打开的最大链接数
FD剧增后带来的IO效率问题
消息传递方式
相同点和不一样点图:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特色:
表面上看epoll的性能最好,
可是在链接数少而且链接都十分活跃的状况下,select和poll的性能可能比epoll好
,毕竟epoll的通知机制须要不少函数回调。
select低效是由于每次它都须要轮询
。但低效也是相对的,视状况而定,也可经过良好的设计改善。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就经过一种机制,能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做。但select,poll,epoll本质上都是同步I/O,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。关于这三种IO多路复用的用法,前面三篇总结写的很清楚,并用服务器回射echo程序进行了测试。
总结:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特色。
一、表面上看epoll的性能最好,可是在链接数少而且链接都十分活跃的状况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制须要不少函数回调。
二、select低效是由于每次它都须要轮询。但低效也是相对的,视状况而定,也可经过良好的设计改善
1 Apache 模型,简称 PPC ( Process Per Connection ,):为每一个链接分配一个进程。主机分配给每一个链接的时间和空间上代价较大,而且随着链接的增多,大量进程间切换开销也增加了。很难应对大量的客户并发链接。
2 TPC 模型( Thread Per Connection ):每一个链接一个线程。和PCC相似。
3 select 模型:I/O多路复用技术。
.1 每一个链接对应一个描述。select模型受限于 FD_SETSIZE即进程最大打开的描述符数linux2.6.35为1024,实际上linux每一个进程所能打开描数字的个数仅受限于内存大小,然而在设计select的系统调用时,倒是参考FD_SETSIZE的值。可经过从新编译内核更改此值,但不能根治此问题,对于百万级的用户链接请求 即使增长相应 进程数, 仍显得杯水车薪呀。
.2select每次都会扫描一个文件描述符的集合,这个集合的大小是做为select第一个参数传入的值。可是每一个进程所能打开文件描述符如果增长了 ,扫描的效率也将减少。
.3内核到用户空间,采用内存复制传递文件描述上发生的信息。
4 poll 模型:I/O多路复用技术。poll模型将不会受限于FD_SETSIZE,由于内核所扫描的文件 描述符集合的大小是由用户指定的,即poll的第二个参数。但仍有扫描效率和内存拷贝问题。
5 pselect模型:I/O多路复用技术。同select。
6 epoll模型:
.1)无文件描述字大小限制仅与内存大小相关
.2)epoll返回时已经明确的知道哪一个socket fd发生了什么事件,不用像select那样再一个个比对。
.3)内核到用户空间采用共享内存方式,传递消息。
一、单个epoll并不能解决全部问题,特别是你的每一个操做都比较费时的时候,由于epoll是串行处理的。 因此你有仍是必要创建线程池来发挥更大的效能。
二、若是fd被注册到两个epoll中时,若是有时间发生则两个epoll都会触发事件。
三、若是注册到epoll中的fd被关闭,则其会自动被清除出epoll监听列表。
四、若是多个事件同时触发epoll,则多个事件会被联合在一块儿返回。
五、epoll_wait会一直监听epollhup事件发生,因此其不须要添加到events中。
六、为了不大数据量io时,et模式下只处理一个fd,其余fd被饿死的状况发生。linux建议能够在fd联系到的结构中增长ready位,而后epoll_wait触发事件以后仅将其置位为ready模式,而后在下边轮询ready fd列表。
协程,又称微线程,纤程。英文名Coroutine。
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中获得普遍应用。
子程序,或者称为函数,在全部语言中都是层级调用,好比A调用B,B在执行过程当中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
因此子程序调用是经过栈实现的,一个线程就是执行一个子程序。
子程序调用老是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不一样。
协程看上去也是子程序,但执行过程当中,在子程序内部可中断,而后转而执行别的子程序,在适当的时候再返回来接着执行。
线程和进程的操做是由程序触发系统接口,最后的执行者是系统;协程的操做则是程序员。
协程能够被认为是一种用户空间线程,与传统的抢占式线程相比,有2个主要的优势:
协程存在的意义:对于多线程应用,CPU经过切片的方式来切换线程间的执行,线程切换时须要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
协程的适用场景:当程序中存在大量不须要CPU的操做时(IO),适用于协程;
协程的好处:
缺点:
进程:
进程之间不共享任何状态,进程的调度由操做系统完成,每一个进程都有本身独立的内存空间,进程间通信主要是经过信号传递的方式来实现的,实现方式有多种,信号量、管道、事件等,任何一种方式的通信效率都须要过内核,致使通信效率比较低。因为是独立的内存空间,上下文切换的时候须要保存先调用栈的信息、cpu各寄存器的信息、虚拟内存、以及打开的相关句柄等信息,因此致使上下文进程间切换开销很大,通信麻烦。
线程:
线程之间共享变量,解决了通信麻烦的问题,可是对于变量的访问须要锁,线程的调度主要也是有操做系统完成,一个进程能够拥有多个线程,可是其中每一个线程会共享父进程像操做系统申请资源,这个包括虚拟内存、文件等,因为是共享资源,因此建立线程所须要的系统资源占用比进程小不少,相应的可建立的线程数量也变得相对多不少。线程时间的通信除了能够使用进程之间通信的方式之外还能够经过共享内存的方式进行通讯,因此这个速度比经过内核要快不少。另外在调度方面也是因为内存是共享的,因此上下文切换的时候须要保存的东西就像对少一些,这样一来上下文的切换也变得高效。
协程:
协程的调度彻底由用户控制,一个线程能够有多个协程,用户建立了几个线程,而后每一个线程都是循环按照指定的任务清单顺序完成不一样的任务,当任务被堵塞的时候执行下一个任务,当恢复的时候再回来执行这个任务,任务之间的切换只须要保存每一个任务的上下文内容,就像直接操做栈同样的,这样就彻底没有内核切换的开销,能够不加锁的访问全局变量,因此上下文的切换很是快;另外协程还须要保证是非堵塞的且没有相互依赖,协程基本上不能同步通信,多采用一步的消息通信,效率比较高。
进程、线程与协程
从硬件发展来看,从最初的单核单CPU,到单核多CPU,多核多CPU,彷佛已经到了极限了,可是单核CPU性能却还在不断提高。server端也在不断的发展变化。若是将程序分为IO密集型应用和CPU密集型应用,两者的server的发展以下:
IO密集型应用: 多进程->多线程->事件驱动->协程
CPU密集型应用:多进程-->多线程
调度和切换的时间:进程 > 线程 > 协程
不须要实现复杂的内存共享且需利用多cpu,用多进程;实现复杂的内存共享及IO密集型应用:多线程或协程;实现复杂的内存共享及CPU密集型应用:协程
咱们首先来简单回顾一下一些经常使用的网络编程模型。网络编程模型能够大致的分为同步模型和异步模型两类。
同步模型使用阻塞IO模式,在阻塞IO模式下调用read等IO函数时会阻塞线程直到IO完成或失败。 同步模型的典型表明是thread_per_connection模型,每当阻塞在主线程上的accept调用返回时则建立一个新的线程去服务于新的socket的读/写。这种模型的优势是程序逻辑简洁,符合人的思惟;缺点是可伸缩性收到线程数的限制,当链接愈来愈多时,线程也愈来愈多,频繁的线程切换会严重拖累性能,同时不得不处理多线程同步的问题。
异步模型通常使用非阻塞IO模式,并配合epoll/select/poll等多路复用机制。在非阻塞模式下调用read,若是没有数据可读则当即返回,并通知用户没有可读(EAGAIN/EWOULDBLOCK),而非阻塞当前线程。异步模型能够使一个线程同时服务于多个IO对象。 异步模型的典型表明是reactor模型。在reactor模型中,咱们将全部要处理的IO事件注册到一个中心的IO多路复用器中(通常为epoll/select/poll),同时主线程阻塞在多路复用器上。一旦有IO事件到来或者就绪,多路复用器返回并将对应的IO事件分发到对应的处理器(即回调函数)中,最后处理器调用read/write函数来进行IO操做。
异步模型的特色是性能和可伸缩性比同步模型要好不少,可是其结构复杂,不易于编写和维护。在异步模型中,IO以前的代码(IO任务的提交者)和IO以后的处理代码(回调函数)是割裂开来的。
协程的出现出现为克服同步模型和异步模型的缺点,并结合他们的优势提供了可能: 如今假设咱们有3个协程A,B,C分别要进行数次IO操做。这3个协程运行在同一个调度器或者说线程的上下文中,并依次使用CPU。调度器在其内部维护了一个多路复用器(epoll/select/poll)。 协程A首先运行,当它执行到一个IO操做,但该IO操做并无当即就绪时,A将该IO事件注册到调度器中,并主动放弃CPU。这时调度器将B切换到CPU上开始执行,一样,当它碰到一个IO操做的时候将IO事件注册到调度器中,并主动放弃CPU。调度器将C切换到cpu上开始执行。当全部协程都被“阻塞”后,调度器检查注册的IO事件是否发生或就绪。假设此时协程B注册的IO时间已经就绪,调度器将恢复B的执行,B将从上次放弃CPU的地方接着向下运行。A和C同理。 这样,对于每个协程来讲,它是同步的模型;可是对于整个应用程序来讲,它是异步的模型。
编程范式(Programming Paradigm)是某种编程语言典型的编程风格或者说是编程方式。随着编程方法学和软件工程研究的深刻,特别是OO思想的普及,范式(Paradigm)以及编程范式等术语渐渐出如今人们面前。面向对象编程(OOP)经常被誉为是一种革命性的思想,正由于它不一样于其余的各类编程范式。编程范式也许是学习任何一门编程语言时要理解的最重要的术语。
托马斯.库恩提出“科学的革命”的范式论以后,Robert Floyd在1979年图灵奖的颁奖演说中使用了编程范式一词。编程范式通常包括三个方面,以OOP为例:
简单的说,编程范式是程序员看待程序应该具备的观点。
为了进一步加深对编程范式的认识,这里介绍几种最多见的编程范式。
须要再次提醒注意的是:编程范式是编程语言的一种分类方式,它并不针对某种编程语言。就编程语言而言,一种编程语言也能够适用多种编程范式。
过程化编程,也被称为命令式编程,应该是最原始的、也是咱们最熟悉的一种传统的编程方式。从本质上讲,它是“冯.诺伊曼机“运行机制的抽象,它的编程思惟方式源于计算机指令的顺序排列。
(也就是说:过程化语言模拟的是计算机机器的系统结构,而并非基于语言的使用者的我的能力和倾向。这一点咱们应该都很清楚,好比:咱们最先曾经使用过的单片机的汇编语言。)
过程化编程的步骤是:
首先,咱们必须将待解问题的解决方案抽象为一系列概念化的步骤。而后经过编程的方式将这些步骤转化为程序指令集(算法),而这些指令按照必定的顺序排列,用来讲明如何执行一个任务或解决一个问题。这就意味着,程序员必需要知道程序要完成什么,而且告诉计算机如何来进行所需的计算工做,包括每一个细节操做。简言之,就是将计算机看做一个有始有终服从命令的装置。
因此在过程化编程中,把待解问题规范化、抽象为某种算法是解决问题的关键步骤。其次,才是编写具体算法和完成相应的算法实现问题的正确解决。固然,程序员对待解问题的抽象能力也是很是重要的因素,但这自己已经与编程语言无关了。
程序流程图是过程化语言进行程序编写的有效辅助手段。
尽管现存的计算机编程语言不少,可是人们把全部支持过程化编程范式的编程语言都被概括为过程化编程语言。例如机器语言、汇编语言、BASIC、COBOL、C 、FORTRAN、语言等等许多第三代编程语言都被概括为过程化语言。
过程化语言特别适合解决线性(或者说循序渐进)的算法问题。它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式很是相似咱们的工做和生活方式,由于咱们的平常活动都是循序渐进的顺序进行的。
过程化语言趋向于开发运行较快且对系统资源利用率较高的程序。过程化语言很是的灵活并强大,同时有许多经典应用范例,这使得程序员能够用它来解决多种问题。
过程化语言的不足之处就是它不适合某些种类问题的解决,例如那些非结构化的具备复杂算法的问题。问题出如今,过程化语言必须对一个算法加以详尽的说明,而且其中还要包括执行这些指令或语句的顺序。实际上,给那些非结构化的具备复杂算法的问题给出详尽的算法是极其困难的。
普遍引发争议和讨论的地方是:无条件分支,或goto语句,它是大多数过程式编程语言的组成部分,反对者声称:goto语句可能被无限地滥用;它给程序设计提供了制造混 乱的机会。目前达成的共识是将它保留在大多数语言中,对于它所具备的危险性,应该经过程序设计的规定将其最小化。
其实,基于事件驱动的程序设计在图形用户界面(GUI)出现好久前就已经被应用于程序设计中,但是只有当图形用户界面普遍流行时,它才逐渐形演变为一种普遍使用的程序设计模式。
在过程式的程序设计中,代码自己就给出了程序执行的顺序,尽管执行顺序可能会受到程序输入数据的影响。
在事件驱动的程序设计中,程序中的许多部分可能在彻底不可预料的时刻被执行。每每这些程序的执行是由用户与正在执行的程序的互动激发所致。
事件驱动经常用于用户与程序的交互,经过图形用户接口(鼠标、键盘、触摸板)进行交互式的互动。固然,也能够用于异常的处理和响应用户自定义的事件等等。
事件的异常处理比用户交互更复杂。
事件驱动不只仅局限在GUI编程应用。可是实现事件驱动咱们还须要考虑更多的实际问题,如:事件定义、事件触发、事件转化、事件合并、事件排队、事件分派、事件处理、事 件连带等等。
其实,到目前为止,咱们尚未找到有关纯事件驱动编程的语言和相似的开发环境。全部关于事件驱动的资料都是基于GUI事件的。
属于事件驱动的编程语言有:VB、C#、Java(Java Swing的GUI)等。它们所涉及的事件绝大多数都是GUI事件。
过程化范式要求程序员用循序渐进的算法看待每一个问题。很显然,并非每一个问题都适合这种过程化的思惟方式。这也就致使了其它程序设计范式出现,包括咱们如今介绍的面向对象的程序设计范式。
面向对象的程序设计模式已经出现二十多年,通过这些年的发展,它的设计思想和设计模式已经稳定的进入编程语言的主流。来自TIOBE Programming Community2010年11月份编程语言排名的前三名Java、C、C++中,Java和C++都是面向对象的编程语言。
面向对象的程序设计包括了三个基本概念:封装性、继承性、多态性。面向对象的程序语言经过类、方法、对象和消息传递,来支持面向对象的程序设计范式。
1. 对象
世间万事万物都是对象。
面向对象的程序设计的抽象机制是将待解问题抽象为面向对象的程序中的对象。利用封装使每一个对象都拥有个体的身份。程序即是成堆的对象,彼此经过消息的传递,请求其它对象 进行工做。
2. 类
每一个对象都是其类中的一个实体。
物以类聚——就是说明:类是类似对象的集合。类中的对象能够接受相同的消息。换句话说:类包含和描述了“具备共同特性(数据元素)和共同行为(功能)”的一组对象。
好比:苹果、梨、橘子等等对象都属于水果类。
3. 封装
封装(有时也被称为信息隐藏)就是把数据和行为结合在一个包中,并对对象的使用者隐藏数据的实现过程。信息隐藏是面向对象编程的基本原则,而封装是实现这一原则的一种方 式。
封装使对象呈现出“黑盒子”特性,这是对象再利用和实现可靠性的关键步骤。
4. 接口
每一个对象都有接口。接口不是类,而是对符合接口需求的类所做的一套规范。接口说明类应该作什么但不指定如何做的方法。一个类能够有一个或多个接口。
5. 方法
方法决定了某个对象究竟可以接受什么样的消息。面向对象的设计有时也会简单地概括为“将消息发送给对象”。
6. 继承
继承的思想就是容许在已存在类的基础上构建新的类。一个子类可以继承父类的全部成员,包括属性和方法。
继承的主要做用:经过实现继承完成代码重用;经过接口继承完成代码被重用。继承是一种规范的技巧,而不是一种实现的技巧。
7. 多态
多态提供了“接口与实现分离”。多态不但能改善程序的组织架构及可读性,更利于开发出“可扩充”的程序。
继承是多态的基础。多态是继承的目的。
合理的运用基于类继承的多态、基于接口继承的多态和基于模版的多态,能加强程序的简洁性、灵活性、可维护性、可重用性和可扩展性。
面向对象技术一方面借鉴了哲学、心理学、生物学的思考方式,另外一方面,它是创建在其余编程技术之上的,是之前的编程思想的天然产物。
若是说结构化软件设计是将函数式编程技术应用到命令式语言中进行程序设计,面向对象编程不过是将函数式模型应用到命令式程序中的另外一途径,此时,模块进步为对象,过程龟缩到class的成员方法中。OOP的不少技术——抽象数据类型、信息隐藏、接口与实现分离、对象生成功能、消息传递机制等等,不少东西就是结构化软件设计所拥有的、或者在其余编程语言中单独出现。但只有在面向对象语言中,他们才共同出现,以一种独特的合做方式互相协做、互相补充。
编程范式 = 语感
知识的学习有几种方式:一种靠记忆,一种靠练习,一种靠培养。就拿英语学习来讲吧,学单词,单靠记忆便可;学句型、语法,光记忆是不够的,需要勤加练习方可熟能生巧;而要讲出地道的英语,光记忆和练习是远远不够的。从小学到大学,甚至博士毕业,除了英语类专业的学生外,大多数人英语练了一二十年,水平如何?不客气但很客观地说:一个字,烂。
缘由只有一个,那就是国内的英语教学方式严重失策。教学老是围绕单词、词组、句型、语法转,缺少对语感的重视和培养,致使学生只会‘中式英语’。一样道理,一个惯用C语言编程的人也许很快就能写一些C++程序,但若是他只注重C++的语法而不注重培养OOP 的语感,那么写出的程序必定是‘C 式C++’。与其如此,倒不如直接用C 呢。”
一句话:学习编程范式能加强编程语言的语感。
语感是一我的对语言的敏锐感知力,反映了他在语言方面的总体上的直觉把握能力。语感强者,能听弦外之音,能说双关之语,能读隽永之做,能写晓畅之文。这是一种综合的素质和修养,其重要性是不言而喻的。那么如何培养语感呢?普通的学习和训练固不可少,但若是忽视语言背后的文化背景和思惟方式,终究只是缘木求鱼。编程范式正体现了编程的思惟方式,于是是培养编程语言的语感的关键。
语感有了,那些设计模式、框架,甚至架构,等看似神秘高深的东西,也会天然而然地来了。
使用yield实现协程操做例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import
time
import
queue
def
consumer(name):
print
(
"--->starting eating baozi..."
)
while
True
:
new_baozi
=
yield
print
(
"[%s] is eating baozi %s"
%
(name,new_baozi))
#time.sleep(1)
def
producer():
r
=
con.__next__()
r
=
con2.__next__()
n
=
0
while
n <
5
:
n
+
=
1
con.send(n)
con2.send(n)
print
(
"\033[32;1m[producer]\033[0m is making baozi %s"
%
n )
if
__name__
=
=
'__main__'
:
con
=
consumer(
"c1"
)
con2
=
consumer(
"c2"
)
p
=
producer()
|
符合什么条件就能称之为协程:
基于上面这4点定义,咱们刚才用yield实现的程并不能算是合格的线程.
greenlet是一个用C实现的协程模块,相比与python自带的yield,它能够使你在任意函数之间随意切换,而不需把这个函数先声明为generator
greelet指的是使用一个任务调度器和一些生成器或者协程实现协做式用户空间多线程的一种伪并发机制,即所谓的微线程。
greelet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操做进行恢复为止。能够使用一个调度器循环在一组生成器函数之间协做多个任务。
网络框架的几种基本的网络I/O模型:
阻塞式单线程:这是最基本的I/O模型,只有在处理完一个请求以后才会处理下一个请求。它的缺点是效能差,若是有请求阻塞住,会让服务没法继续接受请求。可是这种模型编写代码相对简单,在应对访问量不大的状况时是很是适合的。
阻塞式多线程:针对于单线程接受请求量有限的缺点,一个很天然的想法就是给每个请求开一个线程去处理。这样作的好处是可以接受更多的请求,缺点是在线程产生到必定数量以后,进程之间须要大量进行切换上下文的操做,会占用CPU大量的时间,不过这样处理的话编写代码的难道稍高于单进程的状况。
非阻塞式事件驱动:为了解决多线程的问题,有一种作法是利用一个循环来检查是否有网络IO的事件发生,以便决定如何来进行处理(reactor设计模式)。这样的作的好处是进一步下降了CPU的资源消耗。缺点是这样作会让程序难以编写,由于请求接受后的处理过程由reactor来决定,使得程序的执行流程难以把握。当接受到一个请求后若是涉及到阻塞的操做,这个请求的处理就会停下来去接受另外一个请求,程序执行的流程不会像线性程序那样直观。twisted框架就是应用这种IO模型的典型例子。
非阻塞式Coroutine(协程):这个模式是为了解决事件驱动模型执行流程不直观的问题,它在本质上也是事件驱动的,加入了Coroutine的概念。
与线程/进程的区别
线程是抢占式的调度,多个线程并行执行,抢占共同的系统资源;而微线程是协同式的调度。
其实greenlet不是一种真正的并发机制,而是在同一线程内,在不一样函数的执行代码块之间切换,实施“你运行一会、我运行一会”,而且在进行切换时必须指定什么时候切换以及切换到哪。greenlet的接口是比较简单易用的,可是使用greenlet时的思考方式与其余并发方案存在必定区别:
1. 线程/进程模型在大逻辑上一般从并发角度开始考虑,把可以并行处理的而且值得并行处理的任务分离出来,在不一样的线程/进程下运行,而后考虑分离过程可能形成哪些互斥、冲突问题,将互斥的资源加锁保护来保证并发处理的正确性。
2. greenlet则是要求从避免阻塞的角度来进行开发,当出现阻塞时,就显式切换到另外一段没有被阻塞的代码段执行,直到原先的阻塞情况消失之后,再人工切换回原来的代码段继续处理。所以,greenlet本质是一种合理安排了的 串行 。
3. greenlet本质是串行,所以在没有进行显式切换时,代码的其余部分是没法被执行到的,若是要避免代码长时间占用运算资源形成程序假死,那么仍是要将greenlet与线程/进程机制结合使用(每一个线程、进程下均可以创建多个greenlet,可是跨线程/进程时greenlet之间没法切换或通信)。
使用
一个 “greenlet” 是一个很小的独立微线程。能够把它想像成一个堆栈帧,栈底是初始调用,而栈顶是当前greenlet的暂停位置。你使用greenlet建立一堆这样的堆栈,而后在他们之间跳转执行。跳转不是绝对的:一个greenlet必须选择跳转到选择好的另外一个greenlet,这会让前一个挂起,然后一个恢复。两 个greenlet之间的跳转称为 切换(switch) 。
当你建立一个greenlet,它获得一个初始化过的空堆栈;当你第一次切换到它,他会启动指定的函数,而后切换跳出greenlet。当最终栈底 函数结束时,greenlet的堆栈又编程空的了,而greenlet也就死掉了。greenlet也会由于一个未捕捉的异常死掉。
示例:来自官方文档示例
1
2
3
4
5
6
7
8
9
10
11
12
|
from
greenlet
import
greenlet
def
test1():
print
12
gr2.switch()
print
34
def
test2():
print
56
gr1.switch()
print
78
gr1
=
greenlet(test1)
gr2
=
greenlet(test2)
gr1.switch()
|
最后一行跳转到 test1() ,它打印12,而后跳转到 test2() ,打印56,而后跳转回 test1() ,打印34,而后 test1() 就结束,gr1死掉。这时执行会回到原来的 gr1.switch() 调用。注意,78是不会被打印的,由于gr1已死,不会再切换。
基于greenlet的框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# -*- coding:utf-8 -*-
from
greenlet
import
greenlet
def
test1():
print
(
12
)
gr2.switch()
print
(
34
)
gr2.switch()
def
test2():
print
(
56
)
gr1.switch()
print
(
78
)
gr1
=
greenlet(test1)
gr2
=
greenlet(test2)
gr1.switch()
|
Gevent 是一个第三方库,能够轻松经过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet所有运行在主程序操做系统进程的内部,但它们被协做式地调度。
结果:
gevent是一个基于协程(coroutine)的Python网络函数库,经过使用greenlet提供了一个在libev事件循环顶部的高级别并发API。
主要特性有如下几点:
基于libev的快速事件循环,Linux上面的是epoll机制
基于greenlet的轻量级执行单元
API复用了Python标准库里的内容
支持SSL的协做式sockets
可经过线程池或c-ares实现DNS查询
经过monkey patching功能来使得第三方模块变成协做式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import
gevent
def
func1():
print
(
'\033[31;1mTom在跟Jack搞...\033[0m'
)
gevent.sleep(
2
)
print
(
'\033[31;1mTom又回去跟继续跟Jack搞...\033[0m'
)
def
func2():
print
(
'\033[32;1mTom切换到了跟Sunny搞...\033[0m'
)
gevent.sleep(
1
)
print
(
'\033[32;1mTom搞完了Jack,回来继续跟Sunny搞...\033[0m'
)
gevent.joinall([
gevent.spawn(func1),
gevent.spawn(func2),
#gevent.spawn(func3),
])
|
结果:
1
2
3
4
5
6
7
8
|
Tom在跟Jack搞...
Tom切换到了跟Sunny搞...
Tom搞完了Jack,回来继续跟Sunny搞...
Tom又回去跟继续跟Jack搞...
Tom在跟Jack搞...
Tom切换到了跟Sunny搞...
Tom搞完了Jack,回来继续跟Sunny搞...
Tom又回去跟继续跟Jack搞...
|
经过gevent实现单线程下的多socket并发
server side
client side
简单的使用协程写一个爬虫:
串行
并行:
eventlet
eventlet 是基于 greenlet 实现的面向网络应用的并发处理框架,提供“线程”池、队列等与其余 Python 线程、进程模型很是类似的 api,而且提供了对 Python 发行版自带库及其余模块的超轻量并发适应性调整方法,比直接使用 greenlet 要方便得多。
其基本原理是调整 Python 的 socket 调用,当发生阻塞时则切换到其余 greenlet 执行,这样来保证资源的有效利用。须要注意的是:
eventlet 提供的函数只能对 Python 代码中的 socket 调用进行处理,而不能对模块的 C 语言部分的 socket 调用进行修改。对后者这类模块,仍然须要把调用模块的代码封装在 Python 标准线程调用中,以后利用 eventlet 提供的适配器实现 eventlet 与标准线程之间的协做。
虽然 eventlet 把 api 封装成了很是相似标准线程库的形式,但二者的实际并发执行流程仍然有明显区别。在没有出现 I/O 阻塞时,除非显式声明,不然当前正在执行的 eventlet 永远不会把 cpu 交给其余的 eventlet,而标准线程则是不管是否出现阻塞,老是由全部线程一块儿争夺运行资源。全部 eventlet 对 I/O 阻塞无关的大运算量耗时操做基本没有什么帮助。
关于Linux的epoll机制:
epoll是Linux内核为处理大批量文件描述符而做了改进的poll,是Linux下多路复用IO接口select/poll的加强版本,它能显著提升程序在大量并发链接中只有少许活跃的状况下的系统CPU利用率。epoll的优势:
支持一个进程打开大数目的socket描述符。select的一个进程所打开的FD由FD_SETSIZE的设置来限定,而epoll没有这个限制,它所支持的FD上限是最大可打开文件的数目,远大于2048。
IO效率不随FD数目增长而线性降低:因为epoll只会对“活跃”的socket进行操做,因而,只有"活跃"的socket才会主动去调用 callback函数,其余idle状态的socket则不会。
使用mmap加速内核与用户空间的消息传递。epoll是经过内核于用户空间mmap同一块内存实现的。
内核微调。
libev机制
提供了指定文件描述符事件发生时调用回调函数的机制。libev是一个事件循环器:向libev注册感兴趣的事件,好比socket可读事件,libev会对所注册的事件的源进行管理,并在事件发生时触发相应的程序。
官方文档中的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
>>>
import
gevent
>>>
from
gevent
import
socket
>>> urls
=
[
'www.google.com.hk'
,
'www.example.com'
,
'www.python.org'
]
>>> jobs
=
[gevent.spawn(socket.gethostbyname, url)
for
url
in
urls]
>>> gevent.joinall(jobs, timeout
=
2
)
>>> [job.value
for
job
in
jobs]
[
'74.125.128.199'
,
'208.77.188.166'
,
'82.94.164.162'
]
|
注解:gevent.spawn()方法spawn一些jobs,而后经过gevent.joinall将jobs加入到微线程执行队列中等待其完成,设置超时为2秒。执行后的结果经过检查gevent.Greenlet.value值来收集。gevent.socket.gethostbyname()函数与标准的socket.gethotbyname()有相同的接口,但它不会阻塞整个解释器,所以会使得其余的greenlets跟随着无阻的请求而执行。
Monket patching
Python的运行环境容许咱们在运行时修改大部分的对象,包括模块、类甚至函数。虽然这样作会产生“隐式的反作用”,并且出现问题很难调试,但在须要修改Python自己的基础行为时,Monkey patching就派上用场了。Monkey patching可以使得gevent修改标准库里面大部分的阻塞式系统调用,包括socket,ssl,threading和select等模块,而变成协做式运行。
>>> from gevent import monkey ;
>>> monkey . patch_socket ()
>>> import urllib2
经过monkey.patch_socket()方法,urllib2模块能够使用在多微线程环境,达到与gevent共同工做的目的。
事件循环
不像其余网络库,gevent和eventlet相似, 在一个greenlet中隐式开始事件循环。没有必须调用run()或dispatch()的反应器(reactor),在twisted中是有 reactor的。当gevent的API函数想阻塞时,它得到Hub实例(执行时间循环的greenlet),并切换过去。若是没有集线器实例则会动态 建立。
libev提供的事件循环默认使用系统最快轮询机制,设置LIBEV_FLAGS环境变量可指定轮询机制。LIBEV_FLAGS=1为select, LIBEV_FLAGS = 2为poll, LIBEV_FLAGS = 4为epoll,LIBEV_FLAGS = 8为kqueue。
Libev的API位于gevent.core下。注意libev API的回调在Hub的greenlet运行,所以使用同步greenlet的API。能够使用spawn()和Event.set()等异步API。
同步与异步的性能区别
上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn
。 初始化的greenlet列表存放在数组threads
中,此数组被传给gevent.joinall
函数,后者阻塞当前流程,并执行全部给定的greenlet。执行流程只会在 全部greenlet执行完后才会继续向下走。
遇到IO阻塞时会自动切换任务
经过gevent实现单线程下的多socket并发
server side
client side
并发100个sock链接
全部的计算机程序均可以大体分为两类:脚本型(单次运行)和连续运行型(直到用户主动退出)。
脚本型
脚本型的程序包括最先的批处理文件以及使用Python作交易策略回测等等,这类程序的特色是在用户启动后会按照编程时设计好的步骤一步步运行,全部步骤运行完后自动退出。
连续运行型
连续运行型的程序包含了操做系统和绝大部分咱们平常使用的软件等等,这类程序启动后会处于一个无限循环中连续运行,直到用户主动退出时才会结束。
咱们要开发的交易系统就是属于连续运行型程序,而这种程序根据其计算逻辑的运行机制不一样,又能够粗略的分为时间驱动和事件驱动两种。
时间驱动
时间驱动的程序逻辑相对容易设计,简单来讲就是让电脑每隔一段时间自动作一些事情。这个事情自己能够很复杂、包括不少步骤,但这些步骤都是线性的,按照顺序一步步执行下来。
如下代码展现了一个很是简单的时间驱动的Python程序。
1
2
3
4
5
6
7
8
|
from
time
import
sleep
def
demo():
print
u
'时间驱动的程序每隔1秒运行demo函数'
while
1
:
demo()
sleep(
1.0
)
|
时间驱动的程序本质上就是每隔一段时间固定运行一次脚本(上面代码中的demo函数)。尽管脚本自身能够很长、包含很是多的步骤,可是咱们能够看出这种程序的运行机制相对比较简单、容易理解。
举一些量化交易相关的例子:
对速度要求较高的量化交易方面(日内CTA策略、高频策略等等),时间驱动的程序会存在一个很是大的缺点:对数据信息在反应操做上的处理延时。例子2中,在每次逻辑脚本运行完等待的那1秒钟里,程序对于接收到的新数据信息(行情、成交推送等等)是不会作出任何反应的,只有在等待时间结束后脚本再次运行时才会进行相关的计算处理。而处理延时在量化交易中的直接后果就是money:市价单滑点、限价单错过本可成交的价格。
时间驱动的程序在量化交易方面还存在一些其余的缺点:如浪费CPU的计算资源、实现异步逻辑复杂度高等等。
事件驱动
与时间驱动对应的就是事件驱动的程序:当某个新的事件被推送到程序中时(如API推送新的行情、成交),程序当即调用和这个事件相对应的处理函数进行相关的操做。
上面例子2的事件驱动版:交易程序对股指TICK数据进行监听,当没有新的行情过来时,程序保持监听状态不进行任何操做;当收到新的数据时,数据处理函数当即更新K线和其余技术指标,并检查是否知足趋势策略的下单条件执行下单。
对于简单的程序,咱们能够采用上面测试代码中的方案,直接在API的回调函数中写入相应的逻辑。但随着程序复杂度的增长,这种方案会变得愈来愈不可行。假设咱们有一个带有图形界面的量化交易系统,系统在某一时刻接收到了API推送的股指期货行情数据,针对这个数据系统须要进行以下处理:
此时将上面全部的操做都写到一个回调函数中无疑变成了很是差的方案,代码过长容易出错不说,可扩展性也差,每添加一个策略或者功能则又须要修改以前的源代码(有经验的读者会知道,常常修改生产代码是一种很是危险的运营管理方法)。
为了解决这种状况,咱们须要用到事件驱动引擎来管理不一样事件的事件监听函数并执行全部和事件驱动相关的操做。
事件驱动模式能够进一步抽象理解为由事件源,事件对象,以及事件监听器三元素构成,能完成监听器监听事件源、事件源发送事件,监听器收到事件后调用响应函数的动做。
事件驱动主要包含如下元素和操做函数:
元素
1.事件源
2.事件监听器
3.事件对象
操做函数
4.监听动做
5.发送事件
6.调用监听器响应函数
了解清楚了事件驱动的工做原理后,读者能够试着用本身熟悉的编程语言实现,编程主要实现下面的内容,笔者后续给python实现:
用户根据实际业务逻辑定义
事件源 EventSources
监听器 Listeners
事件管理者 EventManager
成员
1.响应函数队列 Handlers
2.事件对象 Event
3.事件对象列表 EventQueue
操做函数
4.监听动做 AddEventListener
5.发送事件 SendEvent
6.调用响应函数 EventProcess
在实际的软件开发过程当中,你会常常看到事件驱动的影子,几乎全部的GUI界面都采用事件驱动编程模型,不少服务器网络模型的消息处理也会采用,甚至复杂点的数据库业务处理也会用这种模型,由于这种模型解耦事件发送者和接收者之间的联系,事件可动态增长减小接收者,业务逻辑越复杂,越能体现它的优点。
论事件驱动模型
在UI编程中,,经常要对鼠标点击进行相应,首先如何得到鼠标点击呢?
方式一:建立一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有如下几个缺点:
方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型,如不少UI平台都会提供onClick()事件,这个事件就表明鼠标按下事件。事件驱动模型大致思路以下:
最初的问题:怎么肯定IO操做完了切回去呢?经过回调函数
在单线程同步模型中,任务按照顺序执行。若是某个任务由于I/O而阻塞,其余全部的任务都必须等待,直到它完成以后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。若是任务之间并无互相依赖的关系,但仍然须要互相等待的话这就使得程序没必要要的下降了运行速度。
在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操做系统来管理,在多处理器系统上能够并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其余线程得以继续执行。与完成相似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,由于这类程序不得不经过线程同步机制如锁、可重入函数、线程局部存储或者其余机制来处理线程安全问题,若是实现不当就会致使出现微妙且使人痛不欲生的bug。
在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其余昂贵的操做时,注册一个回调到事件循环中,而后当I/O操做完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询全部的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽量的得以执行而不须要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,由于程序员不须要关心线程安全问题。
当咱们面对以下的环境时,事件驱动模型一般是一个好的选择:
当应用程序须要在任务间共享可变的数据时,这也是一个不错的选择,由于这里不须要采用同步处理。
网络应用程序一般都有上述这些特色,这使得它们可以很好的契合事件驱动编程模型。
事件驱动编程是指程序的执行流程取决于事件的编程风格,事件由事件处理程序或者事件回调函数进行处理。当某些重要的事件发生时-- 例如数据库查询结果可用或者用户单击了某个按钮,就会调用事件回调函数。
事件驱动编程风格和事件循环相伴相生,事件循环是一个处于不间断循环中的结构,该结构主要具备两项功能-- 事件检测和事件触发处理,在每一轮循环中,它都必须检测发生了什么事件。当事件发生时,事件循环还要决定调用哪一个回调函数。
事件循环只是在一个进程中运行的单个线程,这意味着当事件发生时,能够不用中断就运行事件处理程序,这样作有如下两个特色:
在任一给定时刻,最多运行一个事件处理程序。
事件处理程序能够不间断地运行直到结束。
这使得程序员能放宽同步要求,而且没必要担忧执行并发线程会改变共享内存的状态。
众所周知的秘密
在至关一段时间内,系统编程领域已经知道事件驱动编程是建立处理众多并发链接的服务的最佳方法。众所周知,因为不用保存不少上下文,所以节省了大量内存;又由于也没有那么多上下文切换,又节省了大量执行时间。
大名鼎鼎的Nginx使用了多进程模型,主进程启动时初始化,bind,监听一组sockets,而后fork一堆child processes(workers),workers共享socket descriptor。workers竞争accept_mutex,获胜的worker经过IO multiplex(select/poll/epoll/kqueue/...)来处理成千上万的并发请求。为了得到高性能,Nginx还大量使用了异步,事件驱动,non-blocking IO等技术。"What resulted is amodular, event-driven, asynchronous, single-threaded, non-blockingarchitecture which became the foundation of nginx code."
Nginx 架构
对比着看一下Apache的两种经常使用运行模式,详见 Apache Modules
1. Apache MPM prefork模式
主进程经过进程池维护必定数量(可配置)的worker进程,每一个worker进程负责一个connection。worker进程之间经过竞争mpm-accept mutex实现并发和连接处理隔离。 因为进程内存开销和切换开销,该模式相对来讲是比较低效的并发。
2. Apache MPM worker模式
因为进程开销较大,MPM worker模式作了改进,处理每一个connection的实体改成thread。主进程启动可配数量的子进程,每一个进程启动可配数量的server threads和listen thread。listen threads经过竞争mpm-accept mutex获取到新进的connection request经过queue传递给本身进程所在的server threads处理。因为调度的实体变成了开销较小的thread,worker模式相对prefork具备更好的并发性能。
小结两种webserver,能够发现Nginx使用了更高效的编程模型,worker进程通常跟CPU的core数量至关,每一个worker驻留在一个core上,合理编程能够作到最小程度的进程切换,并且内存的使用也比较经济,基本上没有浪费在进程状态的存储上。而Apache的模式是每一个connection对应一个进程/线程,进程/线程间的切换开销,大量进程/线程的内存开销,cache miss的几率增大,都限制了系统所能支持的并发数。
如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:
1. 保存处理机上下文,包括程序计数器和其余寄存器。
2. 更新PCB信息。
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
4. 选择另外一个进程执行,并更新其PCB。
5. 更新内存管理的数据结构。
6. 恢复处理机上下文。
注:总而言之就是很耗资源,具体的能够参考这篇文章:进程切换
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的
。
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。
缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:
数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。
因为IO的处理速度要远远低于CPU的速度,运行在CPU上的程序不得不考虑IO在准备暑假的过程当中该干点什么,让出CPU给别人仍是本身去干点别的有意义的事情,这就涉及到了采用什么样的IO策略。通常IO策略的选用跟进程线程编程模型要同时考虑,二者是有联系的。
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO能够理解为对流的操做。刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。因此说,当一个read操做发生时,它会经历两个阶段:
第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于socket流而言,
第一步:一般涉及等待网络上的数据分组到达,而后被复制到内核的某个缓冲区。
第二步:把数据从内核缓冲区复制到应用进程缓冲区。
网络应用须要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。
IO介绍
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执行的结果。比如你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现作,须要等5分钟,因而你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。
你说“来个汉堡”,服务员告诉你,汉堡须要等5分钟,你能够先去逛商场,等作好了,咱们再通知你,这样你能够马上去干别的事情(逛商场),这是异步IO。
很明显,使用异步IO来编写程序性能会远远高于同步IO,可是异步IO的缺点是编程模型复杂。想一想看,你得知道何时通知你“汉堡作好了”,而通知你的方法也各不相同。若是是服务员跑过来找到你,这是回调模式,若是服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO。
操做IO的能力都是由操做系统提供的,每一种编程语言都会把操做系统提供的低级C接口封装起来方便使用,Python也不例外。咱们后面会详细讨论Python的IO编程接口。
接触网络编程,咱们时常会与各类与IO相关的概念打交道:同步(Synchronous)、异步(ASynchronous)、阻塞(blocking)和非阻塞(non-blocking)。
同步与异步的主要区别就在于:会不会致使请求进程(或线程)阻塞。同步会使请求进程(或线程)阻塞而异步不会。
linux下有五种常见的IO模型,其中只有一种异步模型,其他皆为同步模型。如图:
同步:
所谓同步,就是在发出一个功能调用时,在没有获得结果以前,该调用就不返回。也就是必须一件一件事作,等前一件作完了才能作下一件事。
例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事
异步:
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能马上获得结果。实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求经过事件触发->服务器处理(这是浏览器仍然能够做其余事情)->处理完毕
这就是同步和异步。举个简单的例子,假若有一个任务包括两个子任务A和B,对于同步来讲,当A在执行的过程当中,B只有等待,直至A执行完毕,B才能执行;而对于异步就是A和B能够并发地执行,B没必要等待A执行完毕以后再执行,这样就不会因为A的执行致使整个任务的暂时等待。
阻塞
阻塞调用是指调用结果返回以前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在获得结果以后才会返回。
有人也许会把阻塞调用和同步调用等同起来,实际上他是不一样的。对于同步调用来讲,不少时候当前线程仍是激活的,只是从逻辑上当前函数没有返回而已。 例如,咱们在socket中调用recv函数,若是缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各类各样的消息。
非阻塞
非阻塞和阻塞的概念相对应,指在不能马上获得结果以前,该函数不会阻塞当前线程,而会马上返回。
对象的阻塞模式和阻塞函数调用
对象是否处于阻塞模式和函数是否是阻塞调用有很强的相关性,可是并非一一对应的。阻塞对象上能够有非阻塞的调用方式,咱们能够经过必定的API去轮询状 态,在适当的时候调用阻塞函数,就能够避免阻塞。而对于非阻塞对象,调用特殊的函数也能够进入阻塞调用。函数select就是这样的一个例子。
这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操做时,若是条件不知足,是会一直等待仍是返回一个标志信息。
1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
2. 异步,就是我调用一个功能,不须要知道该功能结果,该功能有结果后通知我(回调通知)
3. 阻塞, 就是调用我(函数),我(函数)没有接收完数据或者没有获得结果以前,我不会返回。
4. 非阻塞, 就是调用我(函数),我(函数)当即返回,经过select通知调用者
同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否当即返回!
对于举个简单c/s 模式:
同步和异步,阻塞和非阻塞,有些混用,其实它们彻底不是一回事,并且它们修饰的对象也不相同。
阻塞和非阻塞是指当进程访问的数据若是还没有就绪,进程是否须要等待,简单说这至关于函数内部的实现区别,也就是未就绪时是直接返回仍是等待就绪;
而同步和异步是指访问数据的机制,同步通常指主动请求并等待I/O操做完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后即可以继续处理其它任务,随后等待I/O,操做完毕的通知,这能够使进程在数据读写时也不阻塞。(等待"通知")
1)阻塞I/O(blocking I/O)
2)非阻塞I/O (nonblocking I/O)
3) I/O复用(select 和poll) (I/O multiplexing)
4)信号驱动I/O (signal driven I/O (SIGIO))
5)异步I/O (asynchronous I/O (the POSIX aio_functions))
前四种都是同步,只有最后一种才是异步IO。
注:因为signal driven IO在实际中并不经常使用,因此我这只说起剩下的四种IO Model。
在深刻介绍Linux IO各类模型以前,让咱们先来探索一下基本 Linux IO 模型的简单矩阵。以下图所示:
每一个 IO 模型都有本身的使用模式,它们对于特定的应用程序都有本身的优势。本节将简要对其一一进行介绍。常见的IO模型有阻塞、非阻塞、IO多路复用,异步。
阻塞式I/O;
非阻塞式I/O;
I/O复用;
信号驱动式I/O;
异步I/O;
一个输入操做一般包括两个不一样的阶段:
1) 等待数据准备好;
2) 从内核向进程复制数据;
对于一个套接字上的输入操做,第一步一般涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
网络IO操做实际过程涉及到内核和调用这个IO操做的进程。以read为例,read的具体操做分为如下两个部分:
(1)内核等待数据可读
(2)将内核读到的数据拷贝到进程
简介:进程会一直阻塞,直到数据拷贝完成
应用程序调用一个IO函数,致使应用程序阻塞,等待数据准备好。 若是数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
最流行的I/O模型是阻塞式I/O(blocking I/O)模型,默认状况下,全部套接字都是阻塞的。以数据报套接字做为例子,咱们有如图6-1所示的情形。
阻塞I/O模型图:在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。
同步阻塞IO
当调用recv()函数时,系统首先查是否有准备好的数据。若是数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,而后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。
当使用socket()函数和WSASocket()函数建立套接字时,默认的套接字都是阻塞的。这意味着当调用Windows Sockets API不能当即完成时,线程处于等待状态,直到操做完成。
并非全部Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会当即返回。将可能阻塞套接字的Windows Sockets API调用分为如下四种:
1.输入操做: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。若是此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。
2.输出操做: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。若是套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。
3.接受链接:accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的链接请求。若是此时没有链接请求,线程就会进入睡眠状态。
4.外出链接:connect()和WSAConnect()函数。对于TCP链接,客户端以阻塞套接字为参数,调用该函数向服务器发起链接。该函数在收到服务器的应答前,不会返回。这意味着TCP链接总会等待至少到服务器的一次往返时间。
使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当但愿可以当即发送和接收数据,且处理的套接字数量比较少的状况下,使用阻塞模式来开发网络程序比较合适。
阻塞模式套接字的不足表现为,在大量创建好的套接字线程之间进行通讯时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每一个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当但愿同时处理大量套接字时,将无从下手,其扩展性不好
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操做非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误.
咱们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操做没法完成时,不要将进程睡眠,而是返回一个错误。这样咱们的I/O操做函数将不断的测试数据是否已经准备好,若是没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程当中,会大量的占用CPU的时间。
把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数当即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字屡次调用recv()函数的过程。前三次调用recv()函数时,内核数据尚未准备好。所以,该函数当即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。
前三次调用recvfrom时没有数据可返回,所以内核转而当即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,因而recvfrom成功返回。咱们接着处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,咱们称之为轮询(polling)。应用进程只需轮询内核,以查看某个操做是否就绪。这么作每每耗费大量CPU时间。
当使用socket()函数和WSASocket()函数建立套接字时,默认都是阻塞的。在建立套接字以后,经过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().
套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会当即返回。大多数状况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操做在调用期间内没有时间完成。一般,应用程序须要重复调用该函数,直到得到成功返回代码。
须要说明的是并不是全部的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。固然,在调用WSAStartup()函数时更不会返回该错误代码,由于该函数是应用程序第一调用的函数,固然不会返回这样的错误代码。
要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数以外,还能够使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。
因为使用非阻塞套接字在调用函数时,会常常返回WSAEWOULDBLOCK错误。因此在任什么时候候,都应仔细检查返回代码并做好对“失败”的准备。应用程序接二连三地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种作法很浪费系统资源。
要完成这样的操做,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。一样,这种方法也很差。由于该作法对系统形成的开销是很大的,而且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的作法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。
非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,须要编写更多的代码,以便在每一个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。所以,非阻塞套接字便显得有些难于使用。
可是,非阻塞套接字在控制创建的多个链接,在数据的收发量不均,时间不定时,明显具备优点。这种套接字在使用上存在必定难度,但只要排除了这些困难,它在功能上仍是很是强大的。一般状况下,可考虑使用套接字的“I/O模型”,它有助于应用程序经过异步方式,同时对一个或多个套接字的通讯加以管理。
简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并无什么优越性;关键是能实现同时对多个IO端口进行监听;
I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,可是和阻塞I/O所不一样的的,这两个函数能够同时阻塞多个I/O操做。并且能够同时对多个读操做,多个写操做的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操做函数。
咱们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,咱们调用recvfrom把所读数据报复制到应用进程缓冲区。
比较图6-3和图6-1,I/O复用并不显得有什么优点,事实上因为使用select须要两个而不是单个系统调用,I/O复用还稍有劣势。使用select的优点在于咱们能够等待多个描述符就绪。
与I/O复用密切相关的另外一种I/O模型是在多线程中使用阻塞式I/O(咱们常常这么干)。这种模型与上述模型极为类似,但它并无使用select阻塞在多个文件描述符上,而是使用多个线程(每一个文件描述符一个线程),这样每一个线程均可以自由的调用recvfrom之类的阻塞式I/O系统调用了。
简介:两次调用,两次返回;
咱们也能够用信号,让内核在描述符就绪时发送SIGIO信号通知咱们。咱们称这种模型为信号驱动式I/O(signal-driven I/O),图6-4是它的概要展现。
首先咱们容许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,能够在信号处理函数中调用I/O操做函数处理数据。
咱们首先开启套接字的信号驱动式I/O功能,并经过sigaction系统调用安装一个信号处理函数。改系统调用将当即返回,咱们的进程继续工做,也就是说他没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。咱们随后就能够在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理,也能够当即通知主循环,让它读取数据报。
不管如何处理SIGIO信号,这种模型的优点在于等待数据报到达期间进程不被阻塞。主循环能够继续执行,只要等到来自信号处理函数的通知:既能够是数据已准备好被处理,也能够是数据报已准备好被读取。
简介:数据拷贝的时候进程无需阻塞。
异步I/O(asynchronous I/O)由POSIX规范定义。演变成当前POSIX规范的各类早起标准所定义的实时函数中存在的差别已经取得一致。通常地说,这些函数的工做机制是:告知内核启动某个操做,并让内核在整个操做(包括将数据从内核复制到咱们本身的缓冲区)完成后通知咱们。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知咱们什么时候能够启动一个I/O操做,而异步I/O模型是由内核通知咱们I/O操做什么时候完成。图6-5给出了一个例子。
当一个异步过程调用发出后,调用者不能马上获得结果。实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者的输入输出操做
异步非阻塞IO
对比同步非阻塞IO,异步非阻塞IO也有个名字--Proactor。这种策略是真正的异步,使用注册callback/hook函数来实现异步。程序注册本身感兴趣的socket 事件时,同时将处理各类事件的handler也就是对应的函数也注册给内核,不会有任何阻塞式调用。事件发生后内核之间调用对应的handler完成处理。这里暂且理解为内核作了event的调度和handler调用,具体究竟是异步IO库如何作的,如何跟内核通讯的,后续继续研究。
同步IO引发进程阻塞,直至IO操做完成。
异步IO不会引发进程阻塞。
IO复用是先经过select调用阻塞。
咱们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek相似),并告诉内核当整个操做完成时如何通知咱们。该系统调用当即返回,而且在等待I/O完成期间,咱们的进程不被阻塞。本例子中咱们假设要求内核在操做完成时产生某个信号。改信号直到数据已复制到应用进程缓冲区才产生,这一点不一样于信号驱动I/O模型。
图6-6对比了上述5中不一样的I/O模型。能够看出,前4中模型的主要区别在于第一阶段,由于他们的第二阶段是同样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不一样于其余4中模型。
POSIX把这两个术语定于以下:
同步I/O操做(sysnchronous I/O opetation)致使请求进程阻塞,直到I/O操做完成;
异步I/O操做(asynchronous I/O opetation)不致使请求进程阻塞。
根据上述定义,咱们的前4种模型----阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动I/O模型都是同步I/O模型,由于其中真正的I/O操做(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。
IO多路复用之select、poll、epoll详解
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用以下场合:
当客户处理多个描述符时(通常是交互式输入和网络套接口),必须使用I/O复用。
当一个客户同时处理多个套接口时,而这种状况是可能的,但不多出现。
若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。
若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。
若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程
,也没必要维护这些进程/线程,从而大大减少了系统的开销。
目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll
,I/O多路复用就是经过一种机制,一个进程能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做
。但select,pselect,poll,epoll本质上都是同步I/O
,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
对于IO多路复用机制不理解的同窗,能够先行参考《聊聊Linux 五种IO模型》,来了解Linux五种IO模型。
epoll跟select都能提供多路I/O复用的解决方案。在如今的Linux内核里有都可以支持,其中epoll是Linux所特有,而select则应该是POSIX所规定
,通常操做系统均有实现。
基本原理:
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,若是当即返回设为null便可),函数返回。当select函数返回后,能够经过遍历fdset,来找到就绪的描述符。
基本概念
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用以下场合:
(1)当客户处理多个描述字时(通常是交互式输入和网络套接口),必须使用I/O复用。
(2)当一个客户同时处理多个套接口时,而这种状况是可能的,但不多出现。
(3)若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。
(4)若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。
(5)若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程,也没必要维护这些进程/线程,从而大大减少了系统的开销。
select的调用过程以下所示:
(1)使用copy_from_user从用户空间拷贝fd_set到内核空间
(2)注册回调函数__pollwait
(3)遍历全部fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据状况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工做就是把current(当前进程)挂到设备的等待队列中,不一样的设备有不一样的等待队列,对于tcp_poll来讲,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不表明进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操做是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)若是遍历完全部的fd,尚未返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若是超过必定的超时时间(schedule_timeout指定),仍是没人唤醒,则调用select的进程会从新被唤醒得到CPU,进而从新遍历fd,判断有没有就绪的fd。
(8)把fd_set从内核空间拷贝到用户空间。
基本流程,如图所示:
select目前几乎在全部的平台上支持,其良好跨平台支持也是它的一个优势
。select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制
,在Linux上通常为1024,能够经过修改宏定义甚至从新编译内核的方式提高这一限制
,可是这样也会形成效率的下降。
调用select的函数为r, w, e = select.select(rlist, wlist, xlist[, timeout])
,前三个参数都分别是三个列表,数组中的对象均为waitable object
:均是整数的文件描述符(file descriptor)或者一个拥有返回文件描述符方法fileno()
的对象;
rlist
: 等待读就绪的listwlist
: 等待写就绪的listerrlist
: 等待“异常”的list
select方法用来监视文件描述符,若是文件描述符发生变化,则获取该描述符。
二、当 rlist
序列中的描述符发生可读时(accetp和read),则获取发生变化的描述符并添加到 r
序列中
三、当 wlist
序列中含有描述符时,则将该序列中全部的描述符添加到 w
序列中
四、当 errlist
序列中的句柄发生错误时,则将该发生错误的句柄添加到 e
序列中
五、当 超时时间 未设置,则select会一直阻塞,直到监听的描述符发生变化
当 超时时间 =
1
时,那么若是监听的句柄均无任何变化,则select会阻塞
1
秒,以后返回三个空列表,若是监听的描述符(fd)有变化,则直接执行。
六、在list中能够接受Ptython的的file
对象(好比sys.stdin
,或者会被open()
和os.open()
返回的object),socket object将会返回socket.socket()
。也能够自定义类,只要有一个合适的fileno()
的方法(须要真实返回一个文件描述符,而不是一个随机的整数)。
select代码注释
select本质上是经过设置或者检查存放fd标志位的数据结构来进行下一步处理
。这样所带来的缺点是:
select最大的缺陷就是单个进程所打开的FD是有必定限制的,它由FD_SETSIZE设置,默认值是1024。
通常来讲这个数目和系统内存关系很大,
具体数目能够cat /proc/sys/fs/file-max察看
。32位机默认是1024个。64位机默认是2048.
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要经过遍历FD_SETSIZE个Socket来完成调度,无论哪一个Socket是活跃的,都遍历一遍。这会浪费不少CPU时间。
若是能给套接字注册某个回调函数,当他们活跃时,自动完成相关操做,那就避免了轮询
,这正是epoll与kqueue作的。
须要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
select的几大缺点:
(1)每次调用select,都须要把fd集合从用户态拷贝到内核态,这个开销在fd不少时会很大
(2)同时每次调用select都须要在内核遍历传递进来的全部fd,这个开销在fd不少时也很大
(3)select支持的文件描述符数量过小了,默认是1024
基本原理:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间
,而后查询每一个fd对应的设备状态,若是设备就绪则在设备等待队列中加入一项并继续遍历,若是遍历完全部fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了屡次无谓的遍历。
它没有最大链接数的限制,缘由是它是基于链表来存储的
,可是一样有一个缺点:
大量的fd的数组被总体复制于用户态和内核地址空间之间
,而无论这样的复制是否是有意义。
poll还有一个特色是“水平触发”
,若是报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
注意:
从上面看,select和poll都须要在返回后,
经过遍历文件描述符来获取已经就绪的socket
。事实上,同时链接的大量客户端在一时刻可能只有不多的处于就绪状态
,所以随着监视的描述符数量的增加,其效率也会线性降低。
select.poll()
,返回一个poll的对象,支持注册和注销文件描述符。
poll.register(fd[, eventmask])
注册一个文件描述符,注册后,能够经过poll()
方法来检查是否有对应的I/O事件发生。fd
能够是i 个整数,或者有返回整数的fileno()
方法对象。若是File对象实现了fileno(),也能够看成参数使用。
eventmask
是一个你想去检查的事件类型,它能够是常量POLLIN
, POLLPRI
和 POLLOUT
的组合。若是缺省,默认会去检查全部的3种事件类型。
事件常量 | 意义 |
---|---|
POLLIN | 有数据读取 |
POLLPRT | 有数据紧急读取 |
POLLOUT | 准备输出:输出不会阻塞 |
POLLERR | 某些错误状况出现 |
POLLHUP | 挂起 |
POLLNVAL | 无效请求:描述没法打开 |
poll.modify(fd, eventmask)
修改一个已经存在的fd,和poll.register(fd, eventmask)
有相同的做用。若是去尝试修改一个未经注册的fd,会引发一个errno
为ENOENT的IOError
。poll.unregister(fd)
从poll对象中注销一个fd。尝试去注销一个未经注册的fd,会引发KeyError
。poll.poll([timeout])
去检测已经注册了的文件描述符。会返回一个可能为空的list,list中包含着(fd, event)
这样的二元组。 fd
是文件描述符, event
是文件描述符对应的事件。若是返回的是一个空的list,则说明超时了且没有文件描述符有事件发生。timeout
的单位是milliseconds,若是设置了timeout
,系统将会等待对应的时间。若是timeout
缺省或者是None
,这个方法将会阻塞直到对应的poll对象有一个事件发生。poll代码
在linux2.6(准确来讲是2.5.44)由内核直接支持的方法。epoll解决了select和poll的缺点。
epoll同时支持水平触发和边缘触发:
epoll.poll()
会重复通知关注的event,直到与该event有关的全部数据都已被处理。(select, poll是水平触发, epoll默认水平触发)epoll.poll()
的程序必须处理全部和这个event相关的数据,随后的epoll.poll()
调用不会再有这个event的通知。select.epoll([sizehint=-1])
返回一个epoll对象。
eventmask
事件常量 | 意义 |
---|---|
EPOLLIN | 读就绪 |
EPOLLOUT | 写就绪 |
EPOLLPRI | 有数据紧急读取 |
EPOLLERR | assoc. fd有错误状况发生 |
EPOLLHUP | assoc. fd发生挂起 |
EPOLLRT | 设置边缘触发(ET)(默认的是水平触发) |
EPOLLONESHOT | 设置为 one-short 行为,一个事件(event)被拉出后,对应的fd在内部被禁用 |
EPOLLRDNORM | 和 EPOLLIN 相等 |
EPOLLRDBAND | 优先读取的数据带(data band) |
EPOLLWRNORM | 和 EPOLLOUT 相等 |
EPOLLWRBAND | 优先写的数据带(data band) |
EPOLLMSG | 忽视 |
epoll.close()
关闭epoll对象的文件描述符。epoll.fileno
返回control fd的文件描述符number。epoll.fromfd(fd)
用给予的fd来建立一个epoll对象。epoll.register(fd[, eventmask])
在epoll对象中注册一个文件描述符。(若是文件描述符已经存在,将会引发一个IOError
)epoll.modify(fd, eventmask)
修改一个已经注册的文件描述符。epoll.unregister(fd)
注销一个文件描述符。epoll.poll(timeout=-1[, maxevnets=-1])
等待事件,timeout(float)的单位是秒(second)。基本原理:
epoll支持水平触发和边缘触发,最大的特色在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,而且只会通知一次
。还有一个特色是,epoll使用“事件”的就绪通知方式
,经过epoll_ctl注册fd,一旦该fd就绪,内核就会采用相似callback的回调机制来激活该fd
,epoll_wait即可以收到通知。
epoll代码注释
epoll的优势:
没有最大并发链接的限制
,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
效率提高,不是轮询的方式,不会随着FD数目的增长效率降低
。只有活跃可用的FD才会调用callback函数;即Epoll最大的优势就在于它只管你“活跃”的链接,而跟链接总数无关
,所以在实际的网络环境中,Epoll的效率就会远远高于select和poll。
内存拷贝
,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减小复制开销
。
epoll对文件描述符的操做有两种模式:LT(level trigger)和ET(edge trigger)
。LT模式是默认模式,LT模式与ET模式的区别以下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序能够不当即处理该事件
。下次调用epoll_wait时,会再次响应应用程序并通知此事件。ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
应用程序必须当即处理该事件
。若是不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
LT模式
LT(level triggered)是缺省的工做方式,而且同时支持block和no-block socket
。在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你的
。
ET模式
ET(edge-triggered)是高速工做方式,只支持no-block socket
。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使了一个EWOULDBLOCK 错误)。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once)
。
ET模式在很大程度上减小了epoll事件被重复触发的次数,所以效率要比LT模式高
。epoll工做在ET模式的时候,必须使用非阻塞套接口
,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。
在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描
,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制
,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。(此处去掉了遍历文件描述符,而是经过监听回调的的机制。这正是epoll的魅力所在。
)
注意:
若是没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高不少,可是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。
支持一个进程所能打开的最大链接数
FD剧增后带来的IO效率问题
消息传递方式
相同点和不一样点图:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特色:
表面上看epoll的性能最好,
可是在链接数少而且链接都十分活跃的状况下,select和poll的性能可能比epoll好
,毕竟epoll的通知机制须要不少函数回调。
select低效是由于每次它都须要轮询
。但低效也是相对的,视状况而定,也可经过良好的设计改善。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就经过一种机制,能够监视多个描述符,一旦某个描述符就绪(通常是读就绪或者写就绪),可以通知程序进行相应的读写操做。但select,poll,epoll本质上都是同步I/O,由于他们都须要在读写事件就绪后本身负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需本身负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。关于这三种IO多路复用的用法,前面三篇总结写的很清楚,并用服务器回射echo程序进行了测试。
总结:
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特色。
一、表面上看epoll的性能最好,可是在链接数少而且链接都十分活跃的状况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制须要不少函数回调。
二、select低效是由于每次它都须要轮询。但低效也是相对的,视状况而定,也可经过良好的设计改善
1 Apache 模型,简称 PPC ( Process Per Connection ,):为每一个链接分配一个进程。主机分配给每一个链接的时间和空间上代价较大,而且随着链接的增多,大量进程间切换开销也增加了。很难应对大量的客户并发链接。
2 TPC 模型( Thread Per Connection ):每一个链接一个线程。和PCC相似。
3 select 模型:I/O多路复用技术。
.1 每一个链接对应一个描述。select模型受限于 FD_SETSIZE即进程最大打开的描述符数linux2.6.35为1024,实际上linux每一个进程所能打开描数字的个数仅受限于内存大小,然而在设计select的系统调用时,倒是参考FD_SETSIZE的值。可经过从新编译内核更改此值,但不能根治此问题,对于百万级的用户链接请求 即使增长相应 进程数, 仍显得杯水车薪呀。
.2select每次都会扫描一个文件描述符的集合,这个集合的大小是做为select第一个参数传入的值。可是每一个进程所能打开文件描述符如果增长了 ,扫描的效率也将减少。
.3内核到用户空间,采用内存复制传递文件描述上发生的信息。
4 poll 模型:I/O多路复用技术。poll模型将不会受限于FD_SETSIZE,由于内核所扫描的文件 描述符集合的大小是由用户指定的,即poll的第二个参数。但仍有扫描效率和内存拷贝问题。
5 pselect模型:I/O多路复用技术。同select。
6 epoll模型:
.1)无文件描述字大小限制仅与内存大小相关
.2)epoll返回时已经明确的知道哪一个socket fd发生了什么事件,不用像select那样再一个个比对。
.3)内核到用户空间采用共享内存方式,传递消息。
一、单个epoll并不能解决全部问题,特别是你的每一个操做都比较费时的时候,由于epoll是串行处理的。 因此你有仍是必要创建线程池来发挥更大的效能。
二、若是fd被注册到两个epoll中时,若是有时间发生则两个epoll都会触发事件。
三、若是注册到epoll中的fd被关闭,则其会自动被清除出epoll监听列表。四、若是多个事件同时触发epoll,则多个事件会被联合在一块儿返回。五、epoll_wait会一直监听epollhup事件发生,因此其不须要添加到events中。六、为了不大数据量io时,et模式下只处理一个fd,其余fd被饿死的状况发生。linux建议能够在fd联系到的结构中增长ready位,而后epoll_wait触发事件以后仅将其置位为ready模式,而后在下边轮询ready fd列表。