终于要学习Python
中的重头戏—协程了。💪
学习之前我们先看下现在存在的问题:
- CPU的速度远高于IO速度
- IO包括网络访问和本地访问,比如
requests
,urllib
等传统的网络库都是同步的IO - 网络IO大部分的时间都是出于等待的状态,在等待的时候CPU是空闲的,但是又不能执行其他操作
并发、并行、同步、异步、阻塞、非阻塞
这一小节我们就要弄懂上面的这几个名词概念。
什么是并发和并行呢?
并发
:是指一个时间段内(是一段时间),有几个程序在同一个CPU
上运行,但是任意时刻(是一个时间点)只有一个程序在CPU
上运行。
这是因为CPU
的计算速度是很快的,在很多时间内能够完成多个程序间的迅速切换,给我们的感觉就是很多程序在同时运行。
并行
:是指任意时刻点(是一个时间点)上,有多个程序同时运行在多个CPU
上。
并行是不同程序运行在不同的CPU
上,和CPU
的核心数有关。
什么是同步和异步呢?
明确一点:我们在涉及到IO
操作的时候才会涉及到同步,异步,阻塞和非阻塞。
同步
是指代码调用IO
操作时,必须等待IO
操作完成才返回的调用方式。
异步
是指代码调用IO
操作时,不必等IO
操作完成就返回的调用方式。
对于多线程就是典型的异步方式。
什么是阻塞和非阻塞呢?
阻塞
是指调用函数时候当前线程被挂起。
非阻塞
是指调用函数的时候当前线程不会被挂起,而是立即返回。
同步和异步是消息通信的一种机制,可以把操作当成一个消息发送给另一个线程或者协程(多线程的future
)执行某些操作。
阻塞和非阻塞是函数调用的一种机制。
阻塞式请求一个网页
1 | import requests |
在我们请求网页的时候要三次握手建立tcp连接, 等待服务器响应这些都是阻塞操作,这段时间CPU是空闲着的。
IO
多路复用(select、poll、epoll)
学习这小节前我们先抛出一个问题,如何解决多C10k
问题(多用户多并发低配置)?
Unix下五种I/O模型
我们接下来看下五种常用的I/O
模型。
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用
- 信号驱动式I/O
- 异步I/O(POSIX的aio_系列函数)
我们首先看下阻塞式I/O:
对于阻塞式I/O,我们编写代码很方便。但是,我们看到阻塞式I/O有很长的时间去等待数据的准备,严重浪费了CPU
的性能。
阻塞是不会消耗CPU的
我们再看下非阻塞式I/O:
从图中我们看出不再阻塞等待,但是需要不断去访问是否准备好数据,这个过程是消耗CPU
时间的。
对于一个操作是非阻塞式I/O来说,当下面代码不依赖上线返回结果而言,是省时的操作,但是,当依赖返回的话,就需要不断轮询去看是否有返回。
一个小知识点:内核空间和用户空间
操作系统会将内存分为两块,低地址的为内核空间,高地址的为用户空间。平时我们写的程序都是运行在用户空间上,真正和外界进行数据交流的是内核空间,因此我们进行数据请求的过程是必须经过内核空间的,从内核空间拷贝数据到用户空间,同样是耗CPU
和时间的操作。
有没有一种机制就是当操作系统已经准备好数据放到内核空间之后,再去通知我们的程序去拷数据呢?
这种机制就是–I/O复用:
我们从图中看出,应用进程调用select
,poll
,epoll
等待指定时间,从批量被监控的描述符中,获取可以操作的描述符(即数据准备好的描述符),然后对这些描述符进行操作,因为有数据所以不会出现阻塞。
I/O多路复用,应用于大量文件描述符的场景,不适用于单个文件描述符的场景(和阻塞一样了)。
I/O复用同样存在将内核数据拷贝到用户空间的操作,这个操作是耗时的,怎么解决这个问题呢?
我们看下信号驱动式I/O:
对于驱动式I/O,我们建立一个信号处理程序, 然后操作系统在数据准备好之后,主动发一个信号给我们的信号处理程序,处理程序收到信号之后就会通知应用进程调用recvfrom
。
我们最后看下异步I/O:
我们看到异步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类,分别是writefds
、readfds
、和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
好:
在高并发的情况下,连接活跃度不是很高(是指连接后可能随时关闭,比如浏览网页),epoll
比select
好。
并发性不高,同时连接很活跃(比如游戏),select
比epoll
好。
抽象的知识。。。。。。💪