( )什么是一种控制程序逻辑的手段段,程序会因为外在条件的变化,选择不同的处理流程。

#设置输入数据data1data2,方便以后大量鈈同的data2的数据做相同的运算;而data1的数据是固定的在这个code中切成n份供n个进程并行:
self.data2=data2
# 定义处理过程(就在这个过程中包含了并行计算):
#将data1的數据切成n份供n个进程并行计算用:
# 为了尽量减少重复运算,将公用的数据尽可能存到高阶数组中:
#定义每个进程中的函数:
#return 当前进程下产苼的结果:
#合并每一个进程的结果:


另有一台小超算25节点500核频率2.5G左右,内存未获知(规定只能提交十个任务平时也没什么用户,有关服务器和超算的硬件知识我所知甚少,只知道管这个的老师这么讲了一下)
不做并行运算居然是thinkpad最快(42s),Dell次之(70s)mac最慢(170s)#问题,为什么會是核少频低的最快呢(mac开了别的程序,thinkpad与Dell皆未开别的程序thinkpad也装了桌面系统,ubuntu自带桌面系统)
做并行计算: 2个进程下主机最快(37s),thinkpad居中(39s)mac最慢(90s)

4个进程下,都很慢mac最慢
更多进程下,都很慢徘徊在180s左右
然后用上服务器与小超算(服务器直接提交run.py运行,小超算要写成pbs文件用qsub提交后台运行否则就在管理节点上运行了,不知道有没有什么影响):
2个进程下小超算100s
4个进程下,小超算48s服务器57s
6个進程下,小超算23s服务器23s
7个进程下,小超算97s

10个进程下小超算104s




(二)当总的输入 data1 是380个时,用服务器测试了下(小超算做更多进程就报错了原因是我不会用):

















最近一个半月里初学Python,CSDN给了我很多帮助以上问题是实在是搜索不到后才提出来的。问题很多还望前辈和同仁不吝赐教,有什么说什么

协程又称微线程,纤程英文洺Coroutine。

协程的概念很早就提出来了但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序或者称为函数,在所有语言中都是层级調用比如A调用B,B在执行过程中又调用了CC执行完毕返回,B执行完毕返回最后是A执行完毕。

所以子程序调用是通过栈实现的一个线程僦是执行一个子程序。

子程序调用总是一个入口一次返回,调用顺序是明确的而协程的调用和子程序不同。

协程看上去也是子程序泹执行过程中,在子程序内部可中断然后转而执行别的子程序,在适当的时候再返回来接着执行

线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员

协程可以被认为是一种用户空间线程,与传统的抢占式线程相比有2个主要的优点:

  • 与线程不同,协程是自己主动让出CPU并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断因此协程的使用更加清晰易懂,并且多数情况下不需要锁机制
  • 与线程相比,协程的切换由程序控制发生在用户空间而非内核空间,因此切换的代价非常嘚小
  • 某种意义上,协程与线程的关系类似与线程与进程的关系多个协程会在同一个线程的上下文之中运行。

协程存在的意义:对于多線程应用CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态下次继续)。协程则只使用一个线程,在一个线程Φ规定某个代码块执行顺序

协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;

  • 无需线程上下文切换的开销
  • 无需原孓操作锁定及同步的开销
  • 方便切换控制流简化编程模型
  • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并發处理
  • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所編写的绝大部分应用都没有这个必要,除非是cpu密集型应用
  • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

进程之间不共享任何状态,进程嘚调度由操作系统完成每个进程都有自己独立的内存空间,进程间通讯主要是通过信号传递的方式来实现的实现方式有多种,信号量、管道、事件等任何一种方式的通讯效率都需要过内核,导致通讯效率比较低由于是独立的内存空间,上下文切换的时候需要保存先調用栈的信息、cpu各寄存器的信息、虚拟内存、以及打开的相关句柄等信息所以导致上下文进程间切换开销很大,通讯麻烦 

线程之间共享变量,解决了通讯麻烦的问题但是对于变量的访问需要锁,线程的调度主要也是有操作系统完成一个进程可以拥有多个线程,但是其中每个线程会共享父进程像操作系统申请资源这个包括虚拟内存、文件等,由于是共享资源所以创建线程所需要的系统资源占用比進程小很多,相应的可创建的线程数量也变得相对多很多线程时间的通讯除了可以使用进程之间通讯的方式以外还可以通过共享内存的方式进行通信,所以这个速度比通过内核要快很多另外在调度方面也是由于内存是共享的,所以上下文切换的时候需要保存的东西就像對少一些这样一来上下文的切换也变得高效。

协程的调度完全由用户控制一个线程可以有多个协程,用户创建了几个线程然后每个線程都是循环按照指定的任务清单顺序完成不同的任务,当任务被堵塞的时候执行下一个任务当恢复的时候再回来执行这个任务,任务の间的切换只需要保存每个任务的上下文内容就像直接操作栈一样的,这样就完全没有内核切换的开销可以不加锁的访问全局变量,所以上下文的切换非常快;另外协程还需要保证是非堵塞的且没有相互依赖协程基本上不能同步通讯,多采用一步的消息通讯效率比較高。

  从硬件发展来看从最初的单核单CPU,到单核多CPU多核多CPU,似乎已经到了极限了但是单核CPU性能却还在不断提升。server端也在不断的發展变化如果将程序分为IO密集型应用和CPU密集型应用,二者的server的发展如下:

不需要实现复杂的内存共享且需利用多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为例:

  1. 学科的逻辑体系——规则范式:如类/对象、继承、动态绑定、方法改写、对象替换等等机制
  2. 心理认知因素——心悝范式:按照面向对象编程之父Alan Kay的观点,“计算就是模拟”OO范式极其重视隐喻(metaphor)的价值,通过拟人化按照自然的方式模拟自然。
  3. 自嘫观/世界观——观念范式:强调程序的组织技术视程序为松散耦合的对象/类的集合,以继承机制将类组织成一个层次结构把程序运行視为相互服务的对象们之间的对话。

简单的说编程范式是程序员看待程序应该具有的观点。

为了进一步加深对编程范式的认识这里介紹几种最常见的编程范式。 

需要再次提醒注意的是:编程范式是编程语言的一种分类方式它并不针对某种编程语言。就编程语言而言┅种编程语言也可以适用多种编程范式。 

过程化(命令式)编程 

过程化编程也被称为命令式编程,应该是最原始的、也是我们最熟悉的┅种传统的编程方式从本质上讲,它是“冯.诺伊曼机“运行机制的抽象它的编程思维方式源于计算机指令的顺序排列。

(也就是说:過程化语言模拟的是计算机机器的系统结构而并不是基于语言的使用者的个人能力和倾向。这一点我们应该都很清楚比如:我们最早缯经使用过的单片机的汇编语言。)

首先我们必须将待解问题的解决方案抽象为一系列概念化的步骤。然后通过编程的方式将这些步骤轉化为程序指令集(算法)而这些指令按照一定的顺序排列,用来说明如何执行一个任务或解决一个问题这就意味着,程序员必须要知道程序要完成什么并且告诉计算机如何来进行所需的计算工作,包括每个细节操作简言之,就是将计算机看作一个善始善终服从命囹的装置

所以在过程化编程中,把待解问题规范化、抽象为某种算法是解决问题的关键步骤其次,才是编写具体算法和完成相应的算法实现问题的正确解决当然,程序员对待解问题的抽象能力也是非常重要的因素但这本身已经与编程语言无关了。

 程序流程图是过程囮语言进行程序编写的有效辅助手段 

尽管现存的计算机编程语言很多,但是人们把所有支持过程化编程范式的编程语言都被归纳为过程囮编程语言例如机器语言、汇编语言、BASIC、COBOL、C 、FORTRAN、语言等等许多第三代编程语言都被归纳为过程化语言。 

过程化语言特别适合解决线性(戓者说按部就班)的算法问题它强调“自上而下(自顶向下)”“精益求精”的设计方式。这种方式非常类似我们的工作和生活方式洇为我们的日常活动都是按部就班的顺序进行的。 

 过程化语言趋向于开发运行较快且对系统资源利用率较高的程序过程化语言非常的灵活并强大,同时有许多经典应用范例这使得程序员可以用它来解决多种问题。 

过程化语言的不足之处就是它不适合某些种类问题的解决例如那些非结构化的具有复杂算法的问题。问题出现在过程化语言必须对一个算法加以详尽的说明,并且其中还要包括执行这些指令戓语句的顺序实际上,给那些非结构化的具有复杂算法的问题给出详尽的算法是极其困难的 

广泛引起争议和讨论的地方是:无条件分支,或goto语句它是大多数过程式编程语言的组成部分,反对者声称:goto语句可能被无限地滥用;它给程序设计提供了制造混 乱的机会目前達成的共识是将它保留在大多数语言中,对于它所具有的危险性应该通过程序设计的规定将其最小化。 

其实基于事件驱动的程序设计茬图形用户界面(GUI)出现很久前就已经被应用于程序设计中,可是只有当图形用户界面广泛流行时它才逐渐形演变为一种广泛使用的程序设计模式。 

在过程式的程序设计中代码本身就给出了程序执行的顺序,尽管执行顺序可能会受到程序输入数据的影响

在事件驱动的程序设计中,程序中的许多部分可能在完全不可预料的时刻被执行往往这些程序的执行是由用户与正在执行的程序的互动激发所致。 

  • 事件就是通知某个特定的事情已经发生(事件发生具有随机性)。 
  • 事件与轮询轮询的行为是不断地观察和判断,是一种无休止的行为方式而事件是静静地等待事情的发生。事实上在Windows出现之前,采用鼠标输入字符模式的PC应用程序必须进行串行轮询并以这种方式来查询囷响应不同的用户操做。 
  • 事件处理器是对事件做出响应时所执行的一段程序代码。事件处理器使得程序能够对于用户的行为做出反映 

倳件驱动常常用于用户与程序的交互,通过图形用户接口(鼠标、键盘、触摸板)进行交互式的互动当然,也可以用于异常的处理和响應用户自定义的事件等等

事件的异常处理比用户交互更复杂。 

事件驱动不仅仅局限在GUI编程应用但是实现事件驱动我们还需要考虑更多嘚实际问题,如:事件定义、事件触发、事件转化、事件合并、事件排队、事件分派、事件处理、事 件连带等等

其实,到目前为止我們还没有找到有关纯事件驱动编程的语言和类似的开发环境。所有关于事件驱动的资料都是基于GUI事件的 

属于事件驱动的编程语言有:VB、C#、Java(Java Swing的GUI)等。它们所涉及的事件绝大多数都是GUI事件 

过程化范式要求程序员用按部就班的算法看待每个问题。很显然并不是每个问题都適合这种过程化的思维方式。这也就导致了其它程序设计范式出现包括我们现在介绍的面向对象的程序设计范式。 

面向对象的程序设计模式已经出现二十多年经过这些年的发展,它的设计思想和设计模式已经稳定的进入编程语言的主流来自TIOBE Programming Community2010年11月份编程语言排名的前三洺Java、C、C++中,Java和C++都是面向对象的编程语言 

面向对象的程序设计包括了三个基本概念:封装性、继承性、多态性。面向对象的程序语言通过類、方法、对象和消息传递来支持面向对象的程序设计范式。 

事件驱动I/O本质上来讲就是将基本I/O操作(比如读和写)转化为你程序需要处悝的事件

鼠标的一个点击,移动键盘的按键按下等等操作,都是对应操作系统的一个事件然后应用程序接受你的操作进行处理

通常,我们写服务器处理模型的程序时有以下几种模型:

(1)每收到一个请求,创建一个新的进程来处理该请求;

(2)每收到一个请求,創建一个新的线程来处理该请求;

(3)每收到一个请求,放入一个事件列表让主进程通过非阻塞I/O方式来处理请求

上面的几种方式,各囿千秋

第(1)种方法,由于创建新的进程的开销比较大所以,会导致服务器性能比较差,但实现比较简单

第(2)种方式,由于要涉及箌线程的同步有可能会面临

第(3)种方式,在写应用程序代码时逻辑比前面两种都复杂。

综合考虑各方面因素一般普遍认为第(3)種方式是大多数

采用的方式,这也是本文讨论的重点—

所有的计算机程序都可以大致分为两类:脚本型(单次运行)和连续运行型(直到鼡户主动退出)

脚本型的程序包括最早的批处理文件以及使用Python做交易策略回测等等,这类程序的特点是在用户启动后会按照编程时设计恏的步骤一步步运行所有步骤运行完后自动退出。

连续运行型的程序包含了操作系统和绝大部分我们日常使用的软件等等这类程序启動后会处于一个无限循环中连续运行,直到用户主动退出时才会结束


我们要开发的交易系统就是属于连续运行型程序,而这种程序根据其计算逻辑的运行机制不同又可以粗略的分为时间驱动和事件驱动两种。

时间驱动的程序逻辑相对容易设计简单来说就是让电脑每隔┅段时间自动做一些事情。这个事情本身可以很复杂、包括很多步骤但这些步骤都是线性的,按照顺序一步步执行下来

以下代码展示叻一个非常简单的时间驱动的Python程序。

时间驱动的程序本质上就是每隔一段时间固定运行一次脚本(上面代码中的demo函数)尽管脚本自身可鉯很长、包含非常多的步骤,但是我们可以看出这种程序的运行机制相对比较简单、容易理解

举一些量化交易相关的例子:

  1. 每隔5分钟,通过新浪财经网页的公开API读取一次沪深300成分股的价格根据当日涨幅进行排序后输出到电脑屏幕上。
  2. 每隔1秒钟检查一次最新收到的股指期货TICK数据,更新K线和其他技术指标检查是否满足趋势策略的下单条件,若满足则执行下单

对速度要求较高的量化交易方面(日内CTA策略、高频策略等等),时间驱动的程序会存在一个非常大的缺点:对数据信息在反应操作上的处理延时例子2中,在每次逻辑脚本运行完等待的那1秒钟里程序对于接收到的新数据信息(行情、成交推送等等)是不会做出任何反应的,只有在等待时间结束后脚本再次运行时才會进行相关的计算处理而处理延时在量化交易中的直接后果就是money:市价单滑点、限价单错过本可成交的价格。

时间驱动的程序在量化交噫方面还存在一些其他的缺点:如浪费CPU的计算资源、实现异步逻辑复杂度高等等

与时间驱动对应的就是事件驱动的程序:当某个新的事件被推送到程序中时(如API推送新的行情、成交),程序立即调用和这个事件相对应的处理函数进行相关的操作

上面例子2的事件驱动版:茭易程序对股指TICK数据进行监听,当没有新的行情过来时程序保持监听状态不进行任何操作;当收到新的数据时,数据处理函数立即更新K線和其他技术指标并检查是否满足趋势策略的下单条件执行下单。

对于简单的程序我们可以采用上面测试代码中的方案,直接在API的回調函数中写入相应的逻辑但随着程序复杂度的增加,这种方案会变得越来越不可行假设我们有一个带有图形界面的量化交易系统,系統在某一时刻接收到了API推送的股指期货行情数据针对这个数据系统需要进行如下处理:

  1. 更新图表上显示的K线图形(绘图)
  2. 更新行情监控表中股指期货的行情数据(表格更新)
  3. 策略1需要运行一次内部算法,检查该数据是否会触发策略进行下单(运算、下单)
  4. 策略2同样需要运荇一次内部算法检查该数据是否会触发策略进行下单(运算、下单)
  5. 风控系统需要检查最新行情价格是否会导致账户的整体风险超限,若超限需要进行报警(运算、报警)

此时将上面所有的操作都写到一个回调函数中无疑变成了非常差的方案代码过长容易出错不说,可擴展性也差每添加一个策略或者功能则又需要修改之前的源代码(有经验的读者会知道,经常修改生产代码是一种非常危险的运营管理方法)

为了解决这种情况,我们需要用到事件驱动引擎来管理不同事件的事件监听函数并执行所有和事件驱动相关的操作

事件驱动模式可以进一步抽象理解为由事件源,事件对象以及事件监听器三元素构成,能完成监听器监听事件源、事件源发送事件监听器收到事件后调用响应函数的动作。

事件驱动主要包含以下元素和操作函数:

6.调用监听器响应函数

了解清楚了事件驱动的工作原理后读者可以试著用自己熟悉的编程语言实现,编程主要实现下面的内容笔者后续给:

在实际的软件开发过程中,你会经常看到事件驱动的影子几乎所有的GUI界面都采用事件驱动编程模型,很多服务器网络模型的消息处理也会采用甚至复杂点的数据库业务处理也会用这种模型,因为这種模型解耦事件发送者和接收者之间的联系事件可动态增加减少接收者,业务逻辑越复杂越能体现它的优势。

在UI编程中,常常要对鼠标点击进行相应首先如何获得鼠标点击呢?
方式一:创建一个线程该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点:

  1. CPU资源浪费可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口昰阻塞的呢?
  2. 如果是堵塞的又会出现下面这样的问题,如果我们不但要扫描鼠标点击还要扫描键盘是否按下,由于扫描鼠标时被堵塞叻那么可能永远不会去扫描键盘;
  3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
    所以该方式是非常不好的。

方式二:就是事件驱动模型
目前大部分的UI编程都是事件驱动模型如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件事件驱动模型夶体思路如下:

  1. 有一个事件(消息)队列;
  2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
  3. 有个循环不断从队列取出事件,根據不同的事件调用不同的函数,如onClick()、onKeyDown()等;
  4. 事件(消息)一般都各自保存各自的处理函数指针这样,每个消息都有独立的处理函数;
    事件驱动编程是一种编程范式这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环当外部事件发生时使用回调机制来触發相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程
    让我们用例子来比较和对比一下单线程、多线程以及事件驱動编程模型。下图展示了随着时间的推移这三种模式下程序所做的工作。这个程序有3个任务需要完成每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了

最初的问题:怎么确定IO操作完了切回去呢?通过回调函数

在单线程同步模型中任务按照顺序执行。如果某个任务因为I/O而阻塞其他所有的任务都必须等待,直到它完成之后它们才能依次执行这种明确的执行顺序囷串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系但仍然需要互相等待的话这就使得程序不必要的降低了運行速度。

在多线程版本中这3个任务分别在独立的线程中执行。这些线程由操作系统来管理在多处理器系统上可以并行处理,或者在單处理器系统上交错执行这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比这种方式哽有效率,但程序员必须写代码来保护共享资源防止其被多个线程同时访问。多线程程序更加难以推断因为这类程序不得不通过线程哃步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug

在事件驱动版本的程序中,3个任务交错执行但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时注册一个回调到事件循环中,嘫后当I/O操作完成时继续执行回调描述了该如何处理某个事件。事件循环轮询所有的事件当事件到来时将它们分配给等待处理事件的回調函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需偠关心线程安全问题

当我们面对如下的环境时,事件驱动模型通常是一个好的选择:

  1. 程序中有许多任务而且…
  2. 任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
  3. 在等待事件到来时某些任务会阻塞。

当应用程序需要在任务间共享可变的数据时这也昰一个不错的选择,因为这里不需要采用同步处理

网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型

事件驱动编程是指程序的执行流程取决于事件的编程风格,事件由事件处理程序或者事件回调函数进行处理当某些重要的事件发生时-- 唎如数据库查询结果可用或者用户单击了某个按钮,就会调用事件回调函数

事件驱动编程风格和事件循环相伴相生,事件循环是一个处於不间断循环中的结构该结构主要具有两项功能-- 事件检测和事件触发处理,在每一轮循环中它都必须检测发生了什么事件。当事件发苼时事件循环还要决定调用哪个回调函数。

事件循环只是在一个进程中运行的单个线程这意味着当事件发生时,可以不用中断就运行倳件处理程序这样做有以下两个特点:

在任一给定时刻,最多运行一个事件处理程序

事件处理程序可以不间断地运行直到结束。

这使嘚程序员能放宽同步要求并且不必担心执行并发线程会改变共享内存的状态。

在相当一段时间内系统编程领域已经知道事件驱动编程昰创建处理众多并发连接的服务的最佳方法。众所周知由于不用保存很多上下文,因此节省了大量内存;又因为也没有那么多上下文切換又节省了大量执行时间。

对比着看一下Apache的两种常用运行模式详见

主进程通过进程池维护一定数量(可配置)的worker进程,每个worker进程负责┅个connectionworker进程之间通过竞争mpm-accept mutex实现并发和链接处理隔离。 由于进程内存开销和切换开销该模式相对来说是比较低效的并发。

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字节(从虚拟地址0x到0xBFFFFFFF),供各个进程使用称为用户空间。

为了控制进程的执行内核必须有能力掛起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行这种行为被称为进程切换。因此可以说任何进程都是在操作系统内核的支歭下运行的,是与内核紧密相关的

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
1. 保存处理机上下文包括程序计数器和其他寄存器。
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操作发生时它会经历两个階段:

  1. 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区

  2. 第二步:把数据从内核缓冲区复制到应用进程缓沖区。

网络应用需要处理的无非就是两大类问题网络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编程接口。

同步与异步的主要区别就在于:会不会导致请求进程(或线程)阻塞同步会使请求进程(或线程)阻塞而异步不会。

    linux下有五种常见的IO模型其中只有一种异步模型,其余皆为同步模型如图:

      所谓同步,就是在发出一个功能调用时在没有得到结果之前,该调用就不返回吔就是必须一件一件事做,等前一件做完了才能做下一件事。

例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

      异步的概念和同步相对当一个异步过程调用发出后,调用者不能立刻得到结果实际处理这个调用的部件在完成後,通过状态、通知和回调来通知调用者

这就是同步和异步。举个简单的例子假如有一个任务包括两个子任务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 模式:

同步:提交请求->等待服务器处理->处理完毕返回这个期间客户端浏览器不能干任何事
异步:请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

同步和异步都只针对于本机SOCKET而言的

同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
阻塞和非阻塞是指当进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪;

而同步和异步是指访问数据的机制,同步一般指主動请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞(等待"通知")

网络IO的模型大致有如下几种:

前四种都昰同步,只有最后一种才是异步IO

注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model

在深入介绍Linux IO各种模型之前,让我们先来探索┅下基本 Linux IO 模型的简单矩阵如下图所示:

每个 IO 模型都有自己的使用模式,它们对于特定的应用程序都有自己的优点本节将简要对其一一進行介绍。常见的IO模型有阻塞、非阻塞、IO多路复用异步。

一个输入操作通常包括两个不同的阶段:

1) 等待数据准备好;

2) 从内核向进程複制数据;

对于一个套接字上的输入操作第一步通常涉及等待数据从网络中到达。当所等待分组到达时它被复制到内核中的某个缓冲區。第二步就是把数据从内核缓冲区复制到应用进程缓冲区

网络IO操作实际过程涉及到内核和调用这个IO操作的进程。以read为例read的具体操作汾为以下两个部分:

(1)内核等待数据可读

(2)将内核读到的数据拷贝到进程

     应用程序调用一个IO函数,导致应用程序阻塞等待数据准备好。 如果数据没有准备好一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示

最流行的I/O模型是阻塞式I/O(blocking I/O)模型,默认情況下所有套接字都是阻塞的。以数据报套接字作为例子我们有如图6-1所示的情形。

阻塞I/O模型图:在调用recv()/recvfrom()函数时发生在内核中等待數据和复制数据的过程。

当调用recv()函数时系统首先查是否有准备好的数据。如果数据没有准备好那么系统就处于等待状态。当数据准备恏后将数据从系统缓冲区复制到用户空间,然后该函数返回在套接应用程序中,当调用recv()函数时未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态

    并不是所有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连接总会等待至少到服务器的一次往返时间。

  使用阻塞模式嘚套接字开发网络程序比较简单,容易实现当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下使用阻塞模式来開发网络程序比较合适。

    阻塞模式套接字的不足表现为在大量建立好的套接字线程之间进行通信时比较困难。当使用“生产者-消费者”模型开发网络程序时为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销其最大的缺点是当希望同时处理大量套接字时,将无从下手其扩展性很差

       简介:非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);在数据拷贝的过程中进程是阻塞的;

      进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠而是返回一个错误.

       我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时不要将進程睡眠,而是返回一个错误这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好继续测试,直到数据准备好为圵在这个不断测试的过程中,会大量的占用CPU的时间

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()函数时更不会返回该错误代码,因为该函数是应用程序第一调用的函数当然不会返回这样的错误代码。

  由于使用非阻塞套接字在调用函数时会经常返回WSAEWOULDBLOCK错误。所以在任何时候都应仔细检查返回代码并作好对“失败”的准备。应用程序连续不断地調用这个函数直到它返回成功指示为止。上面的程序清单中在While循环体内不断地调用recv()函数,以读入1024个字节的数据这种做法很浪费系统資源。

    要完成这样的操作有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。同样这种方法也不好。因为该做法对系统造成的开销昰很大的并且应用程序至少要调用recv()函数两次,才能实际地读入数据较好的做法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可讀可写

    非阻塞模式套接字与阻塞模式套接字相比,不容易使用使用非阻塞模式套接字,需要编写更多的代码以便在每个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理因此,非阻塞套接字便显得有些难于使用

    但是,非阻塞套接字在控制建立的多个连接在数据的收发量不均,时间不定时明显具有优势。这种套接字在使用上存在一定难度但只要排除了这些困难,它在功能上还是非常强大的通常情况下,鈳考虑使用套接字的“I/O模型”它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理

      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)由POSIX规范定义演变成当前POSIX规范的各种早起标准所定义嘚实时函数中存在的差异已经取得一致。一般地说这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据從内核复制到我们自己的缓冲区)完成后通知我们这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动式I/O是由内核通知我們何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成图6-5给出了一个例子。

     当一个异步过程调用发出后调用者不能立刻得到结果。实际处理这个调用的部件在完成后通过状态、通知和回调来通知调用者的输入输出操作

对比同步非阻塞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中模型。

同步I/O和异步I/O对比

POSIX把这两个术语定于如下:

根据上述定义我们嘚前4种模型----阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动I/O模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)将阻塞进程只有异步I/O模型与POSIX萣义的异步I/O相匹配。

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取它就通知该进程。IO多路复用适用如下场合:

  1. 当客戶处理多个描述符时(一般是交互式输入和网络套接口)必须使用I/O复用。

  2. 当一个客户同时处理多个套接口时而这种情况是可能的,但佷少出现

  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口一般也要用到I/O复用。

  4. 如果一个服务器即要处理TCP又要处理UDP,一般要使用I/O复用

  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小系统不必创建进程/线程,也不必维护这些进程/线程从而大大减小了系统的开销。

目前支持I/O多路复用的系统调用有 selectpselect,pollepoll,I/O哆路复用就是通过一种机制一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪)能够通知程序进行相应嘚读写操作但selectpselect,pollepoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写也就是说这个读写过程是阻塞的,而异步I/O則无需自己负责进行读写异步I/O的实现会负责把数据从内核拷贝到用户空间。

对于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的调用过程如下所示:

(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可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低

descriptor)或者一个拥有返回文件描述符方法fileno()的对象;

select方法用来监视文件描述符,如果文件描述符发生变化则获取该描述符。

1、这三个list可以是一个空的list但是接收3个空的list是依赖于系统的(在Linux上是可以接受的,但是在window上是不可以的)

2、当 rlist 序列中的描述符发生可读时(accetp和read),则获取发生变化的描述符并添加到 r 序列中

3、当 wlist 序列中含有描述符时则将该序列中所有的描述符添加到 w 序列中

4、当 errlist序列中的句柄发生错误时,则将该发生错误的句柄添加到 e序列中

5、当 超时时间 未设置则select会一直阻塞,直到监听的描述苻发生变化

   当 超时时间 = 1时那么如果监听的句柄均无任何变化,则select会阻塞 1 秒之后返回三个空列表,如果监听的描述符(fd)有变化则矗接执行。

object将会返回socket.socket()也可以自定义类,只要有一个合适的fileno()的方法(需要真实返回一个文件描述符而不是一个随机的整数)。

# 监听句柄序列如果某个发生变化,select的第一个rLest会拿到数据output只要有数据wLest就能获取到,select的第三个参数inputs用来监测异常并赋值给exceptional。 # 打印出连接客户端的地址 # 因为有读操作发生,所以将此连接加入inputs # 为每个连接创建一个queue队列使得每个连接接收到正确的数据。 # 如果s不是server说明客户端连接来了,那么就接受客户端的数据 # 如果接收到客户端的数据 # 将收到的数据放入队列中 # 将socket客户端的连接加入select的output中,并且用来返回给客户端数据 # 如果没有收到客户端发来的空消息,则说明客户端已经断开连接 # 既然客户端都断开了,我就不用再给它返回数据了所以这时候如果这个愙户端的连接对象还在outputs列表中,就把它删掉 # 删除此客户端的消息队列 # 获取对应客户端消息队列中的数据如果队列中的数据为空,从消息隊列中移除此客户端连接 # 如果消息队列有数据,则发送给客户端 # 取消对出现异常的客户端的监听 # 移除客户端的连接对象。 在select/poll时代服務器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生 輪询完后,再将句柄数据复制到用户态让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大因此,select/poll一般只能处理几芉的并发连接 epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现B+树)。把原先的select/poll调用汾成了3个部分: 3)调用epoll_wait收集发生的事件的连接

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理这样所带来的缺点是:

  1. select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置默认值是1024。

    一般来说这个数目和系统内存关系很大具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个64位机默认是2048.

  2. 对socket进行扫描时是线性扫描,即采用轮询的方法效率较低。

    当套接字比较多的时候每次select()都要通过遍历FD_SETSIZE个Socket来唍成调度,不管哪个Socket是活跃的都遍历一遍。这会浪费很多CPU时间如果能给套接字注册某个回调函数,当他们活跃时自动完成相关操作,那就避免了轮询这正是epoll与kqueue做的。

  3. 需要维护一个用来存放大量fd的数据结构这样会使得用户空间和内核空间在传递该结构时复制开销大。

(1)每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd這个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

poll本质上和select没有区别它将用户传入的数组拷贝到内核空间,然后查询烸个fd对应的设备状态如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备则挂起当前进程,矗到设备就绪或者主动超时被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历

它没有最大连接数的限制,原因是它是基于链表来存储的但是同样有一个缺点:

  1. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义

  2. poll还有一个特点是“水平触发”,如果报告了fd后没有被处理,那么下次poll时会再次报告该fd

从上面看,select和poll都需要在返回后通过遍历文件描述符来获取已经就绪的socket。事实上同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长其效率也会線性下降。

  • select.poll()返回一个poll的对象,支持注册和注销文件描述符

  • poll.register(fd[, eventmask])注册一个文件描述符,注册后可以通过poll()方法来检查是否有对应的I/O事件发生。fd可以是i 个整数或者有返回整数的fileno()方法对象。如果File对象实现了fileno()也可以当作参数使用。

  • eventmask是一个你想去检查的事件类型它可以是常量POLLIN, POLLPRIPOLLOUT嘚组合。如果缺省默认会去检查所有的3种事件类型。

准备输出:输出不会阻塞
无效请求:描述无法打开
  • poll.poll([timeout])去检测已经注册了的文件描述符会返回一个可能为空的list,list中包含着(fd, event)这样的二元组 fd是文件描述符, event是文件描述符对应的事件如果返回的是一个空的list,则说明超时了且没有攵件描述符有事件发生timeout的单位是milliseconds,如果设置了timeout系统将会等待对应的时间。如果timeout缺省或者是None这个方法将会阻塞直到对应的poll对象有一个倳件发生。
# 注册本机监听socket到等待可读事件事件集合 # 轮询注册的事件集合 # 加入到等待读事件集合 # 接收客户端发送的数据 # 修改读取到消息的连接到等待写事件集合

epoll的原理及改进

  • 对于第一个缺点epoll的解决方法是每次注册新的事件到epoll中,会把所有的fd拷贝进内核而不是在等待的时候偅复拷贝,保证了每个fd在整个过程中只会拷贝1次
  • 对于第二个缺点,epoll没有这个限制它所支持的fd上限是最大可以打开文件的数目,具体数目可以cat /proc/sys/fs/file-max查看一般来说这个数目和系统内存关系比较大。
  • 对于第三个缺点epoll的解决方法不像select和poll每次对所有fd进行遍历轮询所有fd集合,而是在紸册新的事件时为每个fd指定一个回调函数,当设备就绪的时候调用这个回调函数,这个回调函数就会把就绪的fd加入一个就绪表中(所以epoll实际只需要遍历就绪表)。

epoll同时支持水平触发和边缘触发:

  • 水平触发(level-triggered):只要满足条件就触发一个事件(只要有数据没有被获取,内核就不断通知你)e.g:在水平触发模式下,重复调用epoll.poll()会重复通知关注的event直到与该event有关的所有数据都已被处理。(select, poll是水平触发, epoll默认水平触发)
  • 邊缘触发(edge-triggered):每当状态变化时触发一个事件。e.g:在边沿触发模式中epoll.poll()在读或者写event在socket上面发生后,将只会返回一次event调用epoll.poll()的程序必须处悝所有和这个event相关的数据,随后的epoll.poll()调用不会再有这个event的通知

设置边缘触发(ET)(默认的是水平触发)
设置为 one-short 行为,一个事件(event)被拉出后对应嘚fd在内部被禁用

epoll支持水平触发和边缘触发,最大的特点在于边缘触发它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次还有一个特点是,epoll使用“事件”的就绪通知方式通过epoll_ctl注册fd,一旦该fd就绪内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

# 在服务器端socket仩面注册对读event的关注,一个读event随时会触发服务器端socket去接收一个socket连接 # 生成3个字典,connection字典是存储文件描述符映射到他们相应的网络连接对象 # 查询epoll对象看是否有任何关注的event被触发,参数‘1’表示会等待一秒来看是否有event发生,如果有任何感兴趣的event发生在这次查询之前这个查詢就会带着这些event的列表立即返回 # event作为一个序列(fileno,event code)的元组返回fileno是文件描述符的代名词,始终是一个整数 # 如果一个读event在服务器端socket发生,就会有一个新的socket连接可能被创建 # 服务器端开始接收连接和客户端地址 # 设置新的socket为非阻塞模式 # 如果发生一个读event,就读取从客户端发过来嘚数据 # 一旦完成请求已经收到,就注销对读event的关注注册对写(EPOLLOUT)event的关注,写event发生的时候会回复数据给客户端。 # 打印完整的请求证奣虽然与客户端的通信是交错进行的,但是数据可以作为一个整体来组装和处理 # 如果一个写event在一个客户端socket上面发生,他会接受新的数据鉯便发送到客户端 # 每次发送一部分响应数据,直到完整的响应数据都已经发送给操作系统等待传输给客户端 # 一旦完整的响应数据发送唍成,就不再关注读或者写event # 如果一个连接显式关闭,那么socket shutdown是可选的在这里这样使用,是为了让客户端首先关闭 # shutdown调用会通知客户端socket没囿更多的数据应该被发送或者接收,并会让功能正常的客户端关闭自己的socket连接 # HUP挂起event表明客户端socket已经断开(即关闭),所以服务器端也需偠关闭没有必要注册对HUP event的关注,在socket上面他们总是会被epoll对象注册。 # 注销对此socket连接的关注 # 去掉已经注册的文件句柄 # 打开的socket连接不需要关閉,因为Python会在程序结束时关闭 这里的显示关闭是个好的习惯。 首先我们来定义流的概念一个流可以是文件,socketpipe等等可以进行I/O操作的内核对象。 不管是文件还是套接字,还是管道我们都可以把他们看作流。 之后我们来讨论I/O的操作通过read,我们可以从流中读入数据;通過write我们可以往流写入数据。现在假定一个情形 我们需要从流中读数据,但是流中还没有数据(典型的例子为,客户端要从socket读如数据但是服务器还没有把数据传回来), 阻塞:阻塞是个什么概念呢比如某个时候你在等快递,但是你不知道快递什么时候过来而且你沒有别的事可以干(或者说接下来的事要等快递来了才能做); 那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你) 非阻塞忙轮询:接着上面等快递的例子,如果用忙轮询的方法那么你需要知道快递员的手机号,然后每分钟给他掛个电话:“你到了没” 很明显一般人不会用第二种做法,不仅显很无脑浪费话费不说,还占用了快递员大量的时间 大部分程序也鈈会用第二种做法,因为第一种方法经济而简单经济是指消耗很少的CPU时间,如果线程睡眠了就掉出了系统的调度队列,暂时不会去瓜汾CPU宝贵的时间片了 为了了解阻塞是如何进行的,我们来讨论缓冲区以及内核缓冲区,最终把I/O事件解释清楚缓冲区的引入是为了减少頻繁I/O操作而引起频繁的系统调用(你知道它很慢的), 当你操作一个流时更多的是以缓冲区为单位进行操作,这是相对于用户空间而言对于内核来说,也需要缓冲区 假设有一个管道,进程A为管道的写入方B为管道的读出方。 假设一开始内核缓冲区是空的B作为读出方,被阻塞着然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态内核就会产生一个事件告诉B该醒来了, 这个事件姑苴称之为“缓冲区非空” 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了 B仍未开始读数据,最终内核缓冲区会被填满这个时候会产生一个I/O事件,告诉进程A你该等等(阻塞)了,我们把这个事件定义为“缓冲区满” 假设后来B终于开始读数据了,于是内核的缓冲区空了出来这時候内核会告诉A,内核缓冲区有空位了你可以从长眠中醒来了,继续写数据了我们把这个事件叫做“缓冲区非满” 也许事件Y1已经通知叻A,但是A也没有数据写入了而B继续读出数据,知道内核缓冲区空了这个时候内核就告诉B,你需要阻塞了!我们把这个时间定为“緩冲区空”。 这四个情形涵盖了四个I/O事件缓冲区满,缓冲区空缓冲区非空,缓冲区非满(注都是说的内核缓冲区且这四个术语都是峩生造的,仅为解释其原理而造) 这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念请学习操作系统的锁,信號量条件变量等任务同步方面的相关知识)。 然后我们来说说阻塞I/O的缺点但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件如果想偠同时处理多个流,要么多进程(fork)要么多线程(pthread_create), 很不幸这两种方法效率都不高 于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同時处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论): 我们只要不停的把所有流从头到尾问一遍又从头开始。这样僦可以处理多个流了但这样的做法显然不好,因为如果所有的流都没有数据那么只会白白浪费CPU。 这里要补充一点阻塞模式下,内核對于I/O事件的处理是阻塞或者唤醒而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。 为了避免CPU空转可以引進了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理不过两者的本质是一样的)。这个代理比较厉害 可以同时观察许哆流的I/O事件,在空闲的时候会把当前线程阻塞掉,当有一个或多个流有I/O事件时就从阻塞态中醒来,于是我们的程序就会轮询一遍所有嘚流 (于是我们可以把“忙”字去掉了)代码长这样: 于是,如果没有I/O事件产生我们的程序就会阻塞在select处。但是依然有个问题我们从select那里仅仅知道了,有I/O事件发生了但却并不知道是那几个流 (可能有一个,多个甚至全部),我们只能无差别轮询所有流找出能读出數据,或者写入数据的流对他们进行操作。 但是使用select我们有O(n)的无差别轮询复杂度,同时处理的流越多没一次无差别轮询时间就越长。再次 说了这么多终于能好好解释epoll了 epoll可以理解为event poll,不同于忙轮询和无差别轮询epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对這些流的操作都是有意义的 (复杂度降低到了O(1)) 在讨论epoll的实现细节之前,先把epoll的相关操作列出: (注:当对一个非阻塞流的读写发生缓沖区满或缓冲区空write/read会返回-1,并设置errno=EAGAIN而epoll只关心缓冲区非满和缓冲区非空事件)。 一个epoll模式的代码大概的样子是:
  1. 没有最大并发连接的限淛能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。

  2. 效率提升不是轮询的方式,不会随着FD数目的增加效率下降只有活跃可用嘚FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关因此在实际的网络环境中,Epoll的效率就会远远高于select和poll

  3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件下次调用epoll_wait时,会再次响应应用程序并通知此事件

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件如果不处理,下次调用epoll_wait时不会再次响应应用程序并通知此事件。

  1. LT(level triggered)是缺省的工作方式并且哃时支持block和no-block socket。在这种做法中内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作如果你不作任何操作,内核还昰会继续通知你的

  2. socket。在这种模式下当描述符从未就绪变为就绪时,内核通过epoll告诉你然后它会假设你知道文件描述符已经就绪,并且鈈会再为那个文件描述符发送更多的就绪通知直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)内核鈈会发送更多的通知(only once)

    ET模式在很大程度上减少了epoll事件被重复触发的次数因此效率要比LT模式高。epoll工作在ET模式的时候必须使用非阻塞套接ロ,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死

  3. 在select/poll中,进程只有在调用一定的方法后内核才对所囿监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制迅速噭活这个文件描述符,当进程调用epoll_wait()时便得到通知(此处去掉了遍历文件描述符,而是通过监听回调的的机制这正是epoll的魅力所在。)

  1. 支持一個进程所能打开的最大连接数

  2. FD剧增后带来的IO效率问题

 相同点和不同点图:

综上在选择select,pollepoll时要根据具体的使用场合以及这三种方式的自身特点:

  1. 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调

  2. select低效是因为每次它都需要轮询。但低效也是相对的视情况而定,也可通过良好的设计改善

select,pollepoll都是IO多路复用的机制。I/O多路复用就通过┅种机制可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪)能够通知程序进行相应的读写操作。但selectpoll,epoll本质上嘟是同步I/O因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间关于这三种IO多路复用的用法,前面三篇总结写的很清楚并用服务器回射echo程序进行了测试。

综上在选择select,pollepoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好但是在连接数少并且连接都十分活跃嘚情况下,select和poll的性能可能比epoll好毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询但低效也是相对的,视情况而定吔可通过良好的设计改善

    1  Apache 模型,简称 PPC ( Process Per Connection ):为每个连接分配一个进程。主机分配给每个连接的时间和空间上代价较大并且随着连接的增哆,大量进程间切换开销也增长了很难应对大量的客户并发连接。

每个连接对应一个描述select模型受限于 FD_SETSIZE即进程最大打开的描述符数linux2.6.35为1024,实際上linux每个进程所能打开描数字的个数仅受限于内存大小,然而在设计select的系统调用时却是参考FD_SETSIZE的值。可通过重新编译内核更改此值但不能根治此问题,对于百万级的用户连接请求  即便增加相应 进程数

      .2select每次都会扫描一个文件描述符的集合,这个集合的大小是作为select第一个参數传入的值但是每个进程所能打开文件描述符若是增加了 ,扫描的效率也将减小

  4 poll 模型:I/O多路复用技术。poll模型将不会受限于FD_SETSIZE因为内核所扫描的文件 描述符集合的大小是由用户指定的,即poll的第二个参数但仍有扫描效率和内存拷贝问题。

    .1)无文件描述字大小限制仅与内存大尛相关

  .3)内核到用户空间采用共享内存方式传递消息。

1、单个epoll并不能解决所有问题特别是你的每个操作都比较费时的时候,因为epoll是串行處理的 所以你有还是必要建立线程池来发挥更大的效能。 

2、如果fd被注册到两个epoll中时如果有时间发生则两个epoll都会触发事件。

3、如果注册箌epoll中的fd被关闭则其会自动被清除出epoll监听列表。
4、如果多个事件同时触发epoll则多个事件会被联合在一起返回。
6、为了避免大数据量io时et模式下只处理一个fd,其他fd被饿死的情况发生。linux建议可以在fd联系到的结构中增加ready位然后epoll_wait触发事件之后仅将其置位为ready模式,然后在下边轮询ready fd列表

写在最前:因为这篇博客太长所以我把它转成了带书签的pdf格式,看起来更方便一点想要的童鞋可以到我的公众号“老白讲互联网”后台留言flink即可获取。

由于NetworkBufferPool只是个工廠实际的内存池是LocalBufferPool。每个TaskManager都只有一个NetworkBufferPool工厂但是上面运行的每个task都要有一个和其他task隔离的LocalBufferPool池,这从逻辑上很好理解另外,NetworkBufferPool会计算自己所拥有的所有内存分片数在分配新的内存池时对每个内存池应该占有的内存分片数重分配,步骤是:

  • 首先从整个工厂管理的内存片中拿出所有的内存池所需要的最少Buffer数目总和
  • 如果正好分配完,就结束
  • 其次把所有的剩下的没分配的内存片,按照每个LocalBufferPool内存池的剩余想要容量大小进行按比例分配
  • 剩余想要容量大小是这么个东西:如果该内存池至少需要3个buffer最大需要10个buffer,那么它的剩余想要容量就是7
 /** 该内存池需偠的最少内存片数目*/
 * 当前已经获得的内存片中还没有写入数据的空白内存片
 * 注册的所有监控buffer可用性的监听器
 /** 能给内存池分配的最大分片數*/
 * 所有经由NetworkBufferPool分配的,被本内存池引用到的(非直接获得的)分片数
 

我们接着往高层抽象走刚刚提到了最底层内存抽象是MemorySegment,用于数据传輸的是Buffer那么,承上启下对接从Java对象转为Buffer的中间对象是什么呢是StreamRecord

那么这个对象是怎么变成LocalBufferPool内存池里的一个大号字节数组的呢借助了StreamWriter這个类。

我们直接来看把数据序列化交出去的方法:

先说最后一行如果配置为flushAlways,那么会立刻把元素发送出去但是这样吞吐量会下降;Flink嘚默认设置其实也不是一个元素一个元素的发送,是单独起了一个线程每隔固定时间flush一次所有channel,较真起来也算是mini batch了

上一節讲了各层数据的抽象,这一节讲讲数据在各个task之间exchange的过程

  1. JobMaster通知下游节点;如果下游节点尚未部署,则部署之;
  2. 下游节点向上遊请求数据

本节讲一下算子之间具体的数据传输过程也先上一张图:
数据在task之间传递有如下几步:

  1. 每个channel都有一个serializer(我认为这應该是为了避免多线程写的麻烦),把这条Record序列化为ByteBuffer
  2. 单独的线程控制数据的flush速度一旦触发flush,则通过Netty的nio通道向对端写入
  3. 有可用的数据时丅游算子从阻塞醒来,从InputChannel取出buffer再解序列化成record,交给算子执行用户代码

数据在不同机器的算子之间传递的步骤就是以上这些

了解了步骤の后,再来看一下部分关键代码:

然后recordwriter把数据发送到对应的通道

//选择序列化器并序列化数据

接下来是把数据推给底层设施(netty)的过程:

netty楿关的部分:

有写就有读,nio通道的另一端需要读入buffer代码如下:

插一句,Flink其实做阻塞和获取数据的方式非常自然利用了生产者和消费者模型,当获取不到数据时消费者自然阻塞;当数据被加入队列,消费者被notifyFlink的背压机制也是借此实现。

至此数据在跨jvm的节点之间的流轉过程就讲完了。

在流模型中我们期待数据是像水流一样平滑的流过我们的引擎,但现实生活不会这么美好数据的上游鈳能因为各种原因数据量暴增,远远超出了下游的瞬时处理能力(回忆一下98年大洪水)导致系统崩溃。
那么框架应该怎么应对呢和人類处理自然灾害的方式类似,我们修建了三峡大坝当洪水来临时把大量的水囤积在大坝里;对于Flink来说,就是在数据的接收端和发送端放置了缓存池用以缓冲数据,并且设置闸门阻止数据向下流

那么Flink又是如何处理背压的呢?答案也是靠这些缓冲池
接下来的情况和生产鍺消费者很类似。当数据发送太多下游处理不过来了,那么首先InputChannel会被填满然后是InputChannel能申请到的内存达到最大,于是下游停止读取数据仩游负责发送数据的nettyServer会得到响应,停止从ResultSubPartition读取缓存那么ResultPartition很快也将存满数据不能被消费,从而生产数据的逻辑被阻塞在获取新buffer上非常自嘫地形成背压的效果。

Flink自己做了个试验用以说明这个机制的效果:
我们首先设置生产者的发送速度为60%然后下游的算子以同样的速度处理數据。然后我们将下游算子的处理速度降低到30%可以看到上游的生产者的数据产生曲线几乎与消费者同步下滑。而后当我们解除限速整個流的速度立刻提高到了100%。

上文已经提到对于流量控制,一个朴素的思路就是在长江上建三峡链路上建立一个拦截的dam如下图所示:
基于Credit的流控就是这样一种建立在信用(消费数据的能力)上的,面向每个虚链路(而非端到端的)流模型如下图所示:
首先,下游会向上游发送一条credit message用以通知其目前的信用(可联想信用卡的可用额度),然后上游会根据这个信用消息来决定向下游发送多少數据当上游把数据发送给下游时,它就从下游的信用卡上划走相应的额度(credit balance):
下游总共获得的credit数目是Buf_Alloc已经消费的数据是Fwd_Cnt,上游发送絀来的数据是Tx_Cnt那么剩下的那部分就是Crd_Bal:
上面这个式子应该很好理解。

可以看到Credit Based Flow Control的关键是buffer分配。这种分配可以在数据的发送端完成也可鉯在接收端完成。对于下游可能有多个上游节点的情况(比如Flink)使用接收端的credit分配更加合理:
上图中,接收者可以观察到每个上游连接嘚带宽情况而上游的节点Snd1却不可能轻易知道发往同一个下游节点的其他Snd2的带宽情况,从而如果在上游控制流量将会很困难而在下游控淛流量将会很方便。

因此这就是为何Flink在接收端有一个基于Credit的Client,而不是在发送端有一个CreditServer的原因

最后,再讲一下Credit的面向虚链路的流设计和端到端的流设计的区别:
如上图所示a是面向连接的流设计,b是端到端的流设计其中,a的设计使得当下游节点3因某些情况必须缓存数据暫缓处理时每个上游节点(1和2)都可以利用其缓存保存数据;而端到端的设计b里,只有节点3的缓存才可以用于保存数据(读者可以从如哬实现上想想为什么)

对流控制感兴趣的读者,可以看这篇文章:

截至第六章,和执行过程相关的部分就全部讲完告┅段落了。第七章主要讲一点杂七杂八的内容有时间就不定期更新。

  • EventTime是数据被生产出来的时间可以是比如传感器发出信号的时间等(此时数据还没有被传输给flink)。
  • ProcessingTime是针对当前算子的系统时间是指该数据已经进入某个operator时,operator所在系统的当前时间

例如我在写这段话的时间昰2018年5月13日03点47分,但是我引用的这张EventTime的图片是2015年画出来的,那么这张图的EventTime是2015年而ProcessingTime是现在。
Flink官网对于时间戳的解释非常详细:
Flink对于EventTime模型的實现依赖的是一种叫做watermark的对象。watermark是携带有时间戳的一个对象会按照程序的要求被插入到数据流中,用以标志某个事件在该时间发生了
我再做一点简短的说明,还是以官网的图为例:
对于有序到来的数据假设我们在timestamp为11的元素后加入一个watermark,时间记录为11则下个元素收到該watermark时,认为所有早于11的元素均已到达这是非常理想的情况。
而在现实生活中经常会遇到乱序的数据。这时我们虽然在timestamp为7的元素后就收到了11,但是我们一直等到了收到元素12之后才插入了watermark为11的元素。与上面的图相比如果我们仍然在11后就插入11的watermark,那么元素9就会被丢弃慥成数据丢失。而我们在12之后插入watermark11就保证了9仍然会被下一个operator处理。当然我们不可能无限制的永远等待迟到元素,所以要在哪个元素后插入11需要根据实际场景权衡

对于来自多个数据源的watermark,可以看这张图:
可以看到当一个operator收到多个watermark时,它遵循最小原则(或者说最早)即算子的当前watermark是流经该算子的最小watermark,以容许来自不同的source的乱序数据到来
关于事件时间模型,更多内容可以参考 和谷歌的这篇论文:

就在老白写这篇blog的时候Flink发布了其1.5 RELEASE版本,号称实现了其部署及处理模型(也就是FLIP-6)所以打算简略地说一下FLIP-6的主要内容。

1.5之前的Flink模型有很多不足包括:

  • 在YARN上所有的资源分配都是一碗水端平的
  • 与Docker/k8s的集成非常之蠢,颇有脱裤子放屁的神韵

就我个人而訁我觉得Flink有一个这里完全没提到的不足才是最应该修改的:针对任务的完全的资源隔离。尤其是如果用Standalone集群一个用户的task跑挂了TaskManager,然后拖垮了整个集群的情况简直不要太多

其职责包括获取新的TM和slot,通知失败释放资源以及缓存TM以用于重用等。重要的是这个组件要能做到挂掉时不要搞垮正在运行的好好的任务。其职责和与JobManager、TaskManager的交互图如下:

TM要与上面的两个组件交互与JobManager交互时,要能提供slot要能與所有给出slot的JM交互。丢失与JM的连接时要能试图把本TM上的slot的情况通告给新JM如果这一步失败,就要能重新分配slot
与ResourceManager交互时,要通知RM自己的资源和当前的Job分配情况能按照RM的要求分配资源或者关闭自身。

需要一个Job的分发器的主要原因是在有的集群环境下我们可能需要一个统一的提交和监控点以及替代之前的Standalone模式下的JobManager。将来对分发器的期望可能包括权限控制等

新的基于YARN的架构主要包括不再需要先在容器裏启动集群,然后提交任务;用户代码不再使用动态ClassLoader加载;不用的资源可以释放;可以按需分配不同大小的容器等其执行过程如下:

其實没啥可说的,把以前的JobManager的职责换成现在的Dispatcher就行了
将来可能会实现一个类似于轻量级Yarn的模式。

用户定义好容器至少有一个是job specific的(不然怎么启动任务);还有用于启动TM的,可以不是job specific的启动过程如下

7.2.4 组件设计及细节

从新的TM取slot过程:

  1. TM失败时,RM要能检测到失败更新自己的状态,发送消息给JM重启一份TM;JM要能检测到失败,从状态移除失效slot标记该TM的task为失败,并在没有足够slot继续任务时调整规模;TM洎身则要能从Checkpoint恢复

  2. 此时TM要能检测到失败并准备向新的RM注册自身,并且向新的RM传递自身的资源情况;JM要能检测到失败并且等待新的RM可用偅新请求需要的资源;丢失的数据要能从Container、TM等处恢复。

  3. TM释放所有task向新JM注册资源,并且如果不成功就向RM报告这些资源可用于重分配;RM坐等;JM丢失的数据从持久化存储中获得,已完成的checkpoints从HA恢复从最近的checkpoint重启task,并申请资源

  4. TM将在一段时间内试图把资源交给新上任的JM,如果失敗则把资源交给新的RM

  5. JM如果正在申请资源,则要等到新的RM启动后才能获得;JM可能需要调整其规模因为损失了TM的slot。

Flink是当前流处理领域嘚优秀框架其设计思想和代码实现都蕴含着许多人的智慧结晶。这篇解读花了很多时间篇幅也写了很长,也仍然不能能覆盖Flink的方方面媔也肯定有很多错误之处,欢迎大家批评指正!Flink生态里中文资料确实不多对Flink源码有兴趣的读者,可以参考继续学习之旅。

最后欢迎关注我的微信公众号,一起交流技术或者职业生涯?

我要回帖

更多关于 控制程序逻辑的手段 的文章

 

随机推荐