python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)

事件驱动模型

上节问题:
协程:遇到IO操作就切换
问题:它是什么时候切换回来的?怎么确定IO操作完了?

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

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

所以,该方式是非常不好的。
2.就是事件驱动模型
目前大部分的UI编程都是时间驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:

  • 有一个事件(消息)队列;
  • 鼠标按下时,往这个队列中添加一个点击事件(消息);
  • 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
  • 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;

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

IO模型

阻塞IO:
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第1张图片
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第2张图片
当用户进程调用了recvfrom这个系统调用(整个过程只发生一次系统调用),kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,这些接口都是阻塞型的。使用这些接口可以很方便的构建服务器/客户机的模型。下面是一个简单地“一问一答”的服务器。

非阻塞IO(可以通过socket的setblocking设置,默认阻塞,调为False就是非阻塞):
Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第3张图片
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第4张图片
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。

注意有俩点缺点:

  • 非阻塞IO 发送了太多的系统调用了
  • 非阻塞IO不能够及时处理数据

IO多路复用(IO multiplexing):

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第5张图片
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第6张图片
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句:所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。因此select()与非阻塞IO类似。

注意1:select函数返回结果中如果没有文件可读了,那么进程就可以调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。
注意2:select的优势在于可以处理多个连接,不适用于单个连接。

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

while True:
    #select会一直监听socket对象,一旦发生变化就会赋值给r
    r,w,e = select.select([sk,],[],[],5)#r是输入列表,w是输出列表,e是错误列表,5是每个几秒监听

    for i in r:#[sk,]
        conn,add = i.accept()
        print(conn)
        print('hello')
    print('。。。。')

运行结果:
。。。。
。。。。
是每个5秒出现一次。。。。

如果现在这样:

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

while True:
    #select会一直监听socket对象,一旦发生变化就会赋值给r
    r,w,e = select.select([sk,],[],[],5)#r是输入列表,w是输出列表,e是错误列表,5是每个几秒监听

    for i in r:#[sk,]
        # conn,add = i.accept()
        # print(conn)
        print('hello')
    print('。。。。')

如果这时有一个客户端向这个服务端请求连接,会出现什么结果?
运行结果是:
hello
。。。。
hello
。。。。
hello
。。。。
这样不停的运行,这是什么原因呢?这儿得说到IO多路复用select的触发方式。

触发方式:
我们从电子的角度来解释一下:
1.水平触发:
也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知,select就会触发。
2.边缘触发:
只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知。
由此可见select的触发方式是水平触发的。

IO多路复用优势:
它可以同时监听多个连接socket对象

import socket
import select
sk = socket.socket()
sk.bind(('127.0.0.1',9904))
sk.listen(5)
inp = [sk,]

while True:
    r,w,e = select.select(inp,[],[],5)#[sk,conn]

    for i in r:#[sk,]
        conn,add = i.accept()
        print(conn)
        print('hello')
        inp.append(conn)
    print('。。。。')

sk什么时候会有变化,当有新的连接对象时会发生变化,
conn什么时候会变化,当有客服端发送新的消息时,接收到消息conn就会发生变化。

IO多路复用三种方式(同步):
select
poll
epoll
三种方式的区别:

  • 其中select是效率最低的,epoll是最好的。
  • select在windows,linux下都有,而poll和epoll只在linux下有,Windows下没有。
  • select监听有个最大量(1024个),poll相对select只是提升了这个最大连接数,而epoll是从根本上解决了问题。
  • select的执行机制是:当有一个连接接受数据了,但它不知道是哪个连接接受的,所以它会依次遍历去找,这很费开销。
  • epoll的执行机制是:当有连接要接受数据了,它会收到是哪个连接的,然后它就可以直接去找这个连接,这样效率就很高。(nginx内部就是用epoll进行多连接的)

IO多路复用其实就是在单线程下实现并发操作。

异步IO(Asynchronous I/O)

Linux下的asynchronous IO其实用得不多,从内核2.6版本才开始引入。先看一下它的流程:
python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八)_第7张图片
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

异步最大的特点就是:全程无阻塞。

你可能感兴趣的:(python网络编程之事件驱动模型以及IO阻塞,IO非阻塞,IO多路复用,异步IO(八))