协程和异步IO-上

终于要学习Python中的重头戏—协程了。💪

学习之前我们先看下现在存在的问题:

  1. CPU的速度远高于IO速度
  2. IO包括网络访问和本地访问,比如requests,urllib等传统的网络库都是同步的IO
  3. 网络IO大部分的时间都是出于等待的状态,在等待的时候CPU是空闲的,但是又不能执行其他操作
并发、并行、同步、异步、阻塞、非阻塞

这一小节我们就要弄懂上面的这几个名词概念。

什么是并发和并行呢?

并发:是指一个时间段内(是一段时间),有几个程序在同一个CPU上运行,但是任意时刻(是一个时间点)只有一个程序在CPU上运行。

这是因为CPU的计算速度是很快的,在很多时间内能够完成多个程序间的迅速切换,给我们的感觉就是很多程序在同时运行。

并行:是指任意时刻点(是一个时间点)上,有多个程序同时运行在多个CPU上。

并行是不同程序运行在不同的CPU上,和CPU的核心数有关。

什么是同步和异步呢?

明确一点:我们在涉及到IO操作的时候才会涉及到同步,异步,阻塞和非阻塞。

同步是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式。

异步是指代码调用IO操作时,不必等IO操作完成就返回的调用方式。

对于多线程就是典型的异步方式。

什么是阻塞和非阻塞呢?

阻塞是指调用函数时候当前线程被挂起。

非阻塞是指调用函数的时候当前线程不会被挂起,而是立即返回。

同步和异步是消息通信的一种机制,可以把操作当成一个消息发送给另一个线程或者协程(多线程的future)执行某些操作。

阻塞和非阻塞是函数调用的一种机制。

阻塞式请求一个网页

1
2
3
import requests
html = requests.get("http://www.baidu.com").text
print(html)

在我们请求网页的时候要三次握手建立tcp连接, 等待服务器响应这些都是阻塞操作,这段时间CPU是空闲着的。

IO多路复用(select、poll、epoll)

学习这小节前我们先抛出一个问题,如何解决多C10k问题(多用户多并发低配置)?

Unix下五种I/O模型

我们接下来看下五种常用的I/O模型。

  1. 阻塞式I/O
  2. 非阻塞式I/O
  3. I/O复用
  4. 信号驱动式I/O
  5. 异步I/O(POSIX的aio_系列函数)

我们首先看下阻塞式I/O:

image-20180924143108480

对于阻塞式I/O,我们编写代码很方便。但是,我们看到阻塞式I/O有很长的时间去等待数据的准备,严重浪费了CPU的性能。

阻塞是不会消耗CPU的

我们再看下非阻塞式I/O:

image-20180924143544454

从图中我们看出不再阻塞等待,但是需要不断去访问是否准备好数据,这个过程是消耗CPU时间的。

对于一个操作是非阻塞式I/O来说,当下面代码不依赖上线返回结果而言,是省时的操作,但是,当依赖返回的话,就需要不断轮询去看是否有返回。

一个小知识点:内核空间和用户空间

操作系统会将内存分为两块,低地址的为内核空间,高地址的为用户空间。平时我们写的程序都是运行在用户空间上,真正和外界进行数据交流的是内核空间,因此我们进行数据请求的过程是必须经过内核空间的,从内核空间拷贝数据到用户空间,同样是耗CPU和时间的操作。

有没有一种机制就是当操作系统已经准备好数据放到内核空间之后,再去通知我们的程序去拷数据呢?

这种机制就是–I/O复用:

image-20180924145143527

我们从图中看出,应用进程调用select,poll,epoll等待指定时间,从批量被监控的描述符中,获取可以操作的描述符(即数据准备好的描述符),然后对这些描述符进行操作,因为有数据所以不会出现阻塞。

I/O多路复用,应用于大量文件描述符的场景,不适用于单个文件描述符的场景(和阻塞一样了)。

I/O复用同样存在将内核数据拷贝到用户空间的操作,这个操作是耗时的,怎么解决这个问题呢?

我们看下信号驱动式I/O:

image-20180924150221575

对于驱动式I/O,我们建立一个信号处理程序, 然后操作系统在数据准备好之后,主动发一个信号给我们的信号处理程序,处理程序收到信号之后就会通知应用进程调用recvfrom

我们最后看下异步I/O:

image-20180924150618293

我们看到异步I/O是在将数据复制好之后再通知信号处理程序。

相比较I/O复用,性能提升不是很大。

什么是select、epoll、poll呢?

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

关于select

select 函数监视的文件描述符分3类,分别是writefdsreadfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

select 还有个缺点就是每次遍历都是要遍历监听的所有的描述符 有多少遍历多少

关于poll

​ 不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

​ pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

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

关于epoll

​ epoll是在LInux 2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

内部数据结构为红黑树

参考文章:Linux IO模式及 select、poll、epoll详解

小节结束我们来看一个问题,epoll并不代表一定比select好:

​ 在高并发的情况下,连接活跃度不是很高(是指连接后可能随时关闭,比如浏览网页),epollselect好。

​ 并发性不高,同时连接很活跃(比如游戏),selectepoll好。

抽象的知识。。。。。。💪

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!