Python之进程、线程、协程及IO多路复用(二)

引子javascript

在学完协程以后,了解到它最优也是解决IO操做的,那么俩个点、html

协程:遇到IO操做就切换。 
但何时切回去呢?怎么肯定IO操做完了?java

诸多诸多linux

不少程序员可能会考虑使用“线程池”或“链接池”。“线程池”旨在减小建立和销毁线程的频率,其维持必定合理数量的线程,并让空闲的线程从新承担新的执行任务。“链接池”维持链接的缓存池,尽可能重用已有的链接、减小建立和关闭链接的频率。nginx

这两种技术均可以很好的下降系统开销,都被普遍应用不少大型系统,如websphere、tomcat和各类数据库等。可是,“线程池”和“链接池”技术也只是在必定程度上缓解了频繁调用IO接口带来的资源占用。并且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。因此使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“链接池”或许能够缓解部分压力,可是不能解决全部问题。总之,多线程模型能够方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,能够用非阻塞接口来尝试解决这个问题程序员

1、事件驱动模型介绍web

线性模式数据库

传统的编程是以下线性模式的:编程

开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束windows

每个代码块里是完成各类各样事情的代码,但编程者知道代码块A,B,C,D...的执行顺序,惟一可以改变这个流程的是数据。输入不一样的数据,根据条件语句判断,流程或许就改成A--->C--->E...--->结束。每一次程序运行顺序或许都不一样,但它的控制流程是由输入数据和你编写的程序决定的。若是你知道这个程序当前的运行状态(包括输入数据和程序自己),那你就知道接下来甚至一直到结束它的运行流程。

 对于事件驱动型程序模型,它的流程大体以下:

开始--->初始化--->等待

 与上面传统编程模式不一样,事件驱动程序在启动以后,就在那等待,等待什么呢?等待被事件触发。传统编程下也有“等待”的时候,好比在代码块D中,你定义了一个input(),须要用户输入数据。但这与下面的等待不一样,传统编程的“等待”,好比input(),你做为程序编写者是知道或者强制用户输入某个东西的,或许是数字,或许是文件名称,若是用户输入错误,你还须要提醒他,并请他从新输入。事件驱动程序的等待则是彻底不知道,也不强制用户输入或者干什么。只要某一事件发生,那程序就会作出相应的“反应”。这些事件包括:输入信息、鼠标、敲击键盘上某个键还有系统内部定时器触发。

事件驱动模型

一般,写服务器处理模型的程序时,有如下几种模型:

(1)每收到一个请求,建立一个新的进程,来处理该请求; 
(2)每收到一个请求,建立一个新的线程,来处理该请求; 
(3)每收到一个请求,放入一个事件列表,让主进程经过非阻塞I/O方式来处理请求

第三种就是协程、事件驱动的方式,通常广泛认为第(3)种方式是大多数网络服务器采用的方式

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<p onclick="fun()">点我呀</p>


<script type="text/javascript">
    function fun() {
          alert('约吗?')
    }
</script>
</body>

</html>
事件驱动模型之鼠标点击事件

 

在UI编程中,经常要对鼠标点击进行相应,首先如何得到鼠标点击呢?

两种方式:

一、建立一个线程循环检测是否有鼠标点击

      那么这个方式有如下几个缺点:

    1. CPU资源浪费,可能鼠标点击的频率很是小,可是扫描线程仍是会一直循环检测,这会形成不少的CPU资源浪费;若是扫描鼠标点击的接口是阻塞的呢?
    2. 若是是堵塞的,又会出现下面这样的问题,若是咱们不但要扫描鼠标点击,还要扫描键盘是否按下,因为扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
    3. 若是一个循环须要扫描的设备很是多,这又会引来响应时间的问题; 
      因此,该方式是很是很差的。

二、就是事件驱动模型 

目前大部分的UI编程都是事件驱动模型,如不少UI平台都会提供onClick()事件,这个事件就表明鼠标按下事件。事件驱动模型大致思路以下:

    1. 有一个事件(消息)队列;
    2. 鼠标按下时,往这个队列中增长一个点击事件(消息);
    3. 有个循环,不断从队列取出事件,根据不一样的事件,调用不一样的函数,如onClick()、onKeyDown()等;
    4. 事件(消息)通常都各自保存各自的处理函数指针,这样,每一个消息都有独立的处理函数; 

上述了解了下事件驱动模型,那么什么是事件驱动模型

这里写图片描述
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特色是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。 

  1. 让咱们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展现了随着时间的推移,这三种模式下程序所作的工做。这个程序有3个任务须要完成,每一个任务都在等待I/O操做时阻塞自身。阻塞在I/O操做上所花费的时间已经用灰色框标示出来了。 

这里写图片描述

最初的问题:怎么肯定IO操做完了切回去呢?经过回调函数 

复制代码
1.要理解事件驱动和程序,就须要与非事件驱动的程序进行比较。实际上,现代的程序大可能是事件驱动的,好比多线程的程序,确定是事件驱动的。早期则存在许多非事件驱动的程序,这样的程序,在须要等待某个条件触发时,会不断地检查这个条件,直到条件知足,这是很浪费cpu时间的。而事件驱动的程序,则有机会释放cpu从而进入睡眠态(注意是有机会,固然程序也可自行决定不释放cpu),当事件触发时被操做系统唤醒,这样就能更加有效地使用cpu.
2.再说什么是事件驱动的程序。一个典型的事件驱动的程序,就是一个死循环,并以一个线程的形式存在,这个死循环包括两个部分,第一个部分是按照必定的条件接收并选择一个要处理的事件,第二个部分就是事件的处理过程。程序的执行过程就是选择事件和处理事件,而当没有任何事件触发时,程序会因查询事件队列失败而进入睡眠状态,从而释放cpu。 3.事件驱动的程序,一定会直接或者间接拥有一个事件队列,用于存储未能及时处理的事件。 4.事件驱动的程序的行为,彻底受外部输入的事件控制,因此,事件驱动的系统中,存在大量这种程序,并以事件做为主要的通讯方式。 5.事件驱动的程序,还有一个最大的好处,就是能够按照必定的顺序处理队列中的事件,而这个顺序则是由事件的触发顺序决定的,这一特性每每被用于保证某些过程的原子化。 6.目前windows,linux,nucleus,vxworks都是事件驱动的,只有一些单片机多是非事件驱动的。
复制代码

注意,事件驱动的监听事件是由操做系统调用的cpu来完成的

2、IO模型

用协程实现的IO阻塞自动切换,那么协程又是怎么实现的,在原理是是怎么实现的。如何去实现事件驱动的状况下IO的自动阻塞的切换,这个学名叫什么呢? => IO多路复用 
好比socketserver,多个客户端链接,单线程下实现并发效果,就叫多路复用。 

IO模型又划分为: 阻塞IO、非阻塞IO、同步IO、异步IO      它们是如何定义的,之间的区别是什么?

解释以前,声明一些概念:

  • 用户空间和内核空间
  • 进程切换
  • 进程的阻塞
  • 文件描述符
  • 缓存 I/O

用户空间和内核空间

如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。 
操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。 
为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。 
针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。 

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换,这种切换是由操做系统来完成的。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。 
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:

保存处理机上下文,包括程序计数器和其余寄存器。

更新PCB信息。

把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。

选择另外一个进程执行,并更新其PCB。

更新内存管理的数据结构。

恢复处理机上下文。 
注:总而言之就是很耗资源的

进程的阻塞

正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

文件描述符

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。

缓存I/O

缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。用户空间无法直接访问内核空间的,内核态到用户态的数据拷贝 

思考:为何数据必定要先到内核区,直接到用户内存不是更直接吗?
缓存 I/O 的缺点: 

数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。

 

同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不一样的人给出的答案均可能不一样

因为signal driven IO(信号驱动IO模型)在实际中并不经常使用,因此只说起剩下的四种IO Model。

再说一下IO发生时涉及的对象和步骤。
      对于一个network IO (这里咱们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另外一个就是系统内核(kernel)。当一个read操做发生时,它会经历两个阶段:
 1 等待数据准备 (Waiting for the data to be ready)
 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,由于这些IO Model的区别就是在两个阶段上各有不一样的状况。

blocking IO (阻塞IO)

在linux中,默认状况下全部的socket都是blocking,一个典型的读操做流程大概是这样:

      当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来讲,不少时候数据在一开始尚未到达(好比,尚未收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
因此,blocking IO的特色就是在IO执行的两个阶段都被block了。

non-blocking IO(非阻塞IO)

linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:

      从图中能够看出,当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,用户进程实际上是须要不断的主动询问kernel数据好了没有。

 注意:

      在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不同,”非阻塞将大的整片时间的阻塞分红N多的小的阻塞, 因此进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是能够作其余事情的。

      也就是说非阻塞的recvform系统调用调用以后,进程并无被阻塞,内核立刻返回给进程,若是数据还没准备好,此时会返回一个error。进程在返回以后,能够干点别的事情,而后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程一般被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。须要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

IO multiplexing(IO多路复用)

IO multiplexing这个词可能有点陌生,可是若是我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。咱们都知道,select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

      当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。(多说一句。因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

注意1:select函数返回结果中若是有文件可读了,那么进程就能够经过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。

注意2: select的优点在于能够处理多个链接,不适用于单个链接

Asynchronous I/O(异步IO)

linux下的asynchronous IO其实用得不多。先看一下它的流程:

用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。

 

四种IO模型都作了一番简单的介绍

回顾上方问题区别 调用blocking IO会一直block住对应的进程直到操做完成,而non-blocking IO在kernel还准备数据的状况下会马上返回。

          异步IO是一点阻塞都没有的模型,而同步IO则带有阻塞

各个IO Model的比较如图所示:

      通过上面的介绍,会发现non-blocking IO和asynchronous IO的区别仍是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,可是它仍然要求进程去主动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则彻底不一样。它就像是用户进程将整个IO操做交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。

五种IO模型比较:

       

在此,上述对不一样的IO模型进行了阐释和区分,可是只是对它们有了一些概念性的理解,想要让它们融会贯通,还须要再从此的实践中再次加深理解

本章主题是IO多路复用,那么如今咱们进入到章节的内容

3、select poll epoll IO多路复用介绍

 

首先列一下,sellect、poll、epoll三者的区别

select 
select最先于1983年出如今4.2BSD中,它经过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程能够得到这些文件描述符从而进行后续的读写操做。 
select目前几乎在全部的平台上支持 
  
select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制,在Linux上通常为1024,不过能够经过修改宏定义甚至从新编译内核的方式提高这一限制。 
  
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增加。同时,因为网络响应时间的延迟使得大量TCP链接处于非活跃状态,但调用select()会对全部socket进行一次线性扫描,因此这也浪费了必定的开销。
poll 
它和select在本质上没有多大差异,可是poll没有最大文件描述符数量的限制。 
通常也不用它,至关于过渡阶段

epoll 
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持 

没有最大文件描述符数量的限制。 
好比100个链接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。 

(了解)epoll能够同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,若是咱们没有采起行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,可是代码实现至关复杂。 
另外一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。 

因此市面上上见到的所谓的异步IO,好比nginx、Tornado、等,咱们叫它异步IO,其实是IO多路复用。

 

 

 

select与epoll

# 首先咱们来定义流的概念,一个流能够是文件,socket,pipe等等能够进行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方式,咱们发现咱们能够同时处理多个流了(把一个流从阻塞模式切换到非阻塞
# 模式再此不予讨论):
# while true {
# for i in stream[]; {
# if i has data
# read until unavailable
# }
# }
# 咱们只要不停的把全部流从头至尾问一遍,又从头开始。这样就能够处理多个流了,但这样的作法显然很差,由于
# 若是全部的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻
# 塞或者唤醒,而非阻塞模式下则把I/O事件交给其余对象(后文介绍的select以及epoll)处理甚至直接忽略。
#
# 为了不CPU空转,能够引进了一个代理(一开始有一位叫作select的代理,后来又有一位叫作poll的代理,不
# 过二者的本质是同样的)。这个代理比较厉害,能够同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻
# 塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,因而咱们的程序就会轮询一遍全部的流(因而咱们可
# 以把“忙”字去掉了)。代码长这样:
# while true {
# select(streams[])
# for i in streams[] {
# if i has data
# read until unavailable
# }
# }
# 因而,若是没有I/O事件产生,咱们的程序就会阻塞在select处。可是依然有个问题,咱们从select那里仅仅知
# 道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至所有),咱们只能无差异轮询全部流,
# 找出能读出数据,或者写入数据的流,对他们进行操做。
# 可是使用select,咱们有O(n)的无差异轮询复杂度,同时处理的流越多,每一次无差异轮询时间就越长。再次
# 说了这么多,终于能好好解释epoll了
# epoll能够理解为event poll,不一样于忙轮询和无差异轮询,epoll之会把哪一个流发生了怎样的I/O事件通知我
# 们。此时咱们对这些流的操做都是有意义的。
# 在讨论epoll的实现细节以前,先把epoll的相关操做列出:
# epoll_create 建立一个epoll对象,通常epollfd = epoll_create()
# epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增长/删除某一个流的某一个事件
# 好比
# epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
# epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
# epoll_wait(epollfd,...)等待直到注册的事件发生
# (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。
# 而epoll只关心缓冲区非满和缓冲区非空事件)。
# 一个epoll模式的代码大概的样子是:
# while true {
# active_stream[] = epoll_wait(epollfd)
# for i in active_stream[] {
# read or write till unavailable
# }
# }


# 举个例子:
#    select:
#          班里三十个同窗在考试,谁先作完想交卷都要经过按钮来活动,他按按钮做为老师的我桌子上的灯就会变红.
#          一旦灯变红,我(select)我就能够知道有人交卷了,可是我并不知道谁交的,因此,我必须跟个傻子似的轮询
#          地去问:嘿,是你要交卷吗?而后我就能够以这种效率极低地方式找到要交卷的学生,而后把它的卷子收上来.
#
#
#    epoll:
#         此次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就能够直接去对应学生那收卷就
#         好了.固然,同时能够有多人交卷.
View Code

 

IO多路复用触发方式

# 在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别以下:
#
# 水平触发:若是文件描述符已经就绪能够非阻塞的执行IO操做了,此时会触发通知.容许在任意时刻重复检测IO的状态,
# 没有必要每次描述符就绪后尽量多的执行IO.select,poll就属于水平触发.
#
# 边缘触发:若是文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽量
# 多的执行IO操做,由于若是在一次通知中没有执行完IO那么就须要等到下一次新的IO活动到来才能获取到就绪的描述
# 符.信号驱动式IO就属于边缘触发.
#
# epoll既能够采用水平触发,也能够采用边缘触发.
#
# 你们可能还不能彻底了解这两种模式的区别,咱们能够举例说明:一个管道收到了1kb的数据,epoll会当即返回,此时
# 读了512字节数据,而后再次调用epoll.这时若是是水平触发的,epoll会当即返回,由于有数据准备好了.若是是边
# 缘触发的不会当即返回,由于此时虽然有数据可读可是已经触发了一次通知,在此次通知到如今尚未新的数据到来,
# 直到有新的数据到来epoll才会返回,此时老的数据和新的数据均可以读取到(固然是须要此次你尽量的多读取).


# 下面咱们还从电子的角度来解释一下:
# 
#     水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能获得通知.上面提到的只要
# 有数据可读(描述符就绪)那么水平触发的epoll就当即返回.
# 
#     边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即便有数据
# 可读,可是没有新的IO活动到来,epoll也不会当即返回.
水平触发和边缘触发

 

实例来袭。。。

实例一

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',6667))
sk.listen(5)
sk.setblocking(False)  # 解除标识位,让accept再也不阻塞

while True:
    try:
        print ('waiting client connection .......')
        connection,address = sk.accept()   # 进程主动轮询
        print("+++",address)
        client_messge = connection.recv(1024)
        print(str(client_messge,'utf8'))
        connection.close()
    except Exception as e:
        print (e)
        time.sleep(4)  # 再然后能够进行别的执行流程 往返轮询

###################################

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
    sk.connect(('127.0.0.1',6667))
    print("hello")
    sk.sendall(bytes("hello","utf8"))
    time.sleep(2)
    break
非阻塞IO

 

优势:可以在等待任务完成的时间里干其余活了(包括提交其余任务,也就是 “后台” 能够有多个任务在同时执行)。

缺点:任务完成的响应延迟增大了,由于每过一段时间才去轮询一次read操做,而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。

实例二

IO multiplexing(多路复用IO):

在非阻塞实例中,轮询的主语是进程,而“后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。不过,这个监听的重任经过调用select等函数交给了内核去作。IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于—前者能够等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,而后进程再进行recvfrom系统调用,将数据由内核拷贝到用户进程,固然这个过程是阻塞的。

import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",9904))
sk.listen(5)

while True:
    r,w,e=select.select([sk,],[],[],5)
    for i in r:
        # conn,add=i.accept()
        #print(conn)
        print("hello")
    print('>>>>>>')
    
#*************************client.py
import socket

sk=socket.socket()

sk.connect(("127.0.0.1",9904))

while 1:
    inp=input(">>").strip()
    sk.send(inp.encode("utf8"))
    data=sk.recv(1024)
    print(data.decode("utf8"))
select多路复用IO

 

问题,为何去掉accept 会反复陷入死循环    select是水平触发

实例三

select实现并发聊天

#***********************server.py
import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",8801))
sk.listen(5)
inputs=[sk,]
while True:
    r,w,e=select.select(inputs,[],[],5)
    print(len(r))

    for obj in r:
        if obj==sk:
            conn,add=obj.accept()
            print(conn)
            inputs.append(conn)
        else:
            data_byte=obj.recv(1024)
            print(str(data_byte,'utf8'))
            inp=input('回答%s号客户>>>'%inputs.index(obj))
            obj.sendall(bytes(inp,'utf8'))

    print('>>',r)

#***********************client.py

import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8801))

while True:
    inp=input(">>>>")
    sk.sendall(bytes(inp,"utf8"))
    data=sk.recv(1024)
    print(str(data,'utf8'))
server端并发聊天

 

文件描述符其实就是我们平时说的句柄,只不过文件描述符是linux中的概念。注意,咱们的accept或recv调用时即向系统发出recvfrom请求

    (1)  若是内核缓冲区没有数据--->等待--->数据到了内核缓冲区,转到用户进程缓冲区;

    (2) 若是先用select监听到某个文件描述符对应的内核缓冲区有了数据,当咱们再调用accept或recv时,直接将数据转到用户缓冲区。

思考1:开启5个client,分别按54321的顺序发送消息,那么server端是按什么顺序回消息的呢?

答: ......

思考2:  如何在某一个client端退出后,不影响server端和其它客户端正常交流

答: 某客户端退出以后,设置一个异常处理,捕获这个客户端退出的异常,并删除select监听的conn

# linux:

if not data_byte:
            inputs.remove(obj)
            continue

# windows

try:
      data_byte=obj.recv(1024)
      print(str(data_byte,'utf8'))
      inp=input('回答%s号客户>>>'%inputs.index(obj))
      obj.sendall(bytes(inp,'utf8'))
except Exception:
      inputs.remove(obj)
View Code

 

4、异步IO

 

import selectors
import socket

sel = selectors.DefaultSelector()


def accept(sock,mask):
    conn,addr = sock.accept()
    conn.setblocking(False)
    sel.register(conn,selectors.EVENT_READ,read)


def read(conn,mask):
    try:
        data = conn.recv(1024)
        if not data:raise Exception
        print(data.decode("utf8"))
        conn.send(b"Hello")
    except Exception as e:
        print(e)
        sel.unregister(conn)
        conn.close()


sock = socket.socket()
sock.bind(("127.0.0.1",8080))
sock.listen(5)
sock.setblocking(False)  # 标识位改成False则再也不阻塞

sel.register(sock,selectors.EVENT_READ,accept)  # 注册绑定

while True:
    events = sel.select()  # 监听
    for key,mask in events:
        callback = key.data
        callback(key.fileobj,mask)
异步IO例子

 

 

 

5、阐释一下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也不例外。

IO编程都是同步模式,异步IO因为复杂度过高。
复制代码
相关文章
相关标签/搜索