一、unix下的五种IO模型阻塞式IO: 进程一直阻塞,直到数据拷贝完成 。应用程序调用一个IO函数,导致应用程序阻塞并等待数据准备就绪。如果数据没有准备好,就一直等待。如果数据准备好了,则数据从内核拷贝到用户空间,IO函数返回成功指示。进程在调用recvfrom开始到它返回的整段时间内是被阻塞的,该函数成功返回后,应用进程开始处理数据报。
非阻塞式IO:数据就绪之前一直轮询 。我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不会阻塞而是立即返回。然后I/O操作函数要不断的检查 数据是否已经准备好,如果没有准备好,继续检查,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom 时,我们称此过程为轮询(polling)。由于应用进程像这样连续不断地查询内核,看看某操作是否准备好,这对CPU时间是极大的浪费,所以这种模型只是偶尔才会遇到。如果IO操作接下来的操作是做计算任务或者再次发起其他的连接请求,使用非阻塞式IO就有优势。
IO多路复用:新增了系统调用select、poll、epoll, 帮助进程监控多个I/O。 I/O复用模型会用到select、poll、epoll函数。select、poll函数也会使进程阻塞,但是和阻塞I/O所不同的是,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行轮询,直到有数据可读或可写时,才真正调用I/O操作函数。 只要有数据就绪,select调用返回,应用程序调用recvfrom将数据从内核区拷贝至用户区。
信号驱动式IO:进程通过接收到的信号确认数据准备就绪 。我们可以用信号,让内核在数据就绪时用信号SIGIO通知我们。首先,我们允许套接字进行信号驱动I/O,并通过系统调用 sigaction安装一个信号处理程序。此系统调用立即返回,进程继续工作,它是非阻塞的。当数据报准备好被读时,就为该进程生成一个SIGIO信号。我们随即可以在信号处理程序中调用 recvfrom 来取读数据报。
异步IO:进程不受阻塞,将任务交给内核处理 。我们让内核启动操作,并在整个操作完成后(包括将数据从内核拷贝到我们自己的缓冲区)通知我们。调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到用户缓冲区后,再通知应用程序。
二、IO多路复用
2.1 select
select前后进行了两次系统调用,第一次调用select,select阻塞结束返回后,可以获得多个准备就绪的套接字(即一个select可以对多个套接字进行管理,类似于同时监控多个套接字事件是否就绪。
监视的文件描述符有3类,writefds、readfds、exceptfds。调用后select会阻塞,直到有描述符就绪(有数据可读、可写、或者有except),或者超时,函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
和阻塞IO模型相比,selectI/O复用模型相当于提前阻塞了。等到有数据到来时,再调用recv就不会因为要等数据就绪而发生阻塞。
缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但这样会造成效率的降低。
2.2 poll
不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式,同时,pollfd并没有最大数量限制(但是数量过大后性能也会下降)。和select函数一样,poll返回后,需要轮询poolfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
2.3 epoll
相对于select和poll来说,epoll更加灵活,没有描述符的限制。epoll使用一个文件描述符管理多个文件描述符,将用户关系的文件描述符的事件存放到内核的一个事件中,这样在用户空间和内核空间的copy只需一次。
查询的数据结构是红黑树。
2.4 三者区别
一个进程能打开的最大连接数select:由FD_SETSIZE宏定义,32位机器上就是32×32,64位机器上就是32×64
poll:本质上和select没有区别,但是没有最大连接数限制,因为基于链表存储
epoll:连接数有上限,但是很大,1G内存可以打开10万左右连接。
FD剧增后带来的IO效率问题select:每次调用都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度线性下降。
poll:同上。
epoll:因为epoll内核中实现是根据每个FD上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃的socket较少的情况下,使用epoll没有前面两者的线性下降的问题,但是如果所有的socket都很活跃可能会有性能问题。
消息传递方式select:内核需要将消息传递到用户空间,都需要内核拷贝动作。
poll:同上。
epoll:epoll通过内核和用户空间共享一块内存来实现的。
如何选择并发高,连接活跃度不高,epoll更好
并发不高,连接活跃,select或者poll更好
三、使用IO多路复用+回调+事件循环完成http请求
3.1 selectors
使用selectors代替select完成http请求:selectors是在select基础上更好的封装
selectors包中的DefaultSelector可以根据平台自主选用IO复用方式,其中选择顺序为epoll|kqueue|devpoll > poll > select.
select本身是不支持register模式selectors实现了注册机制,register(fileobj, events, date=None)fileobj:文件描述符
events:事件,如EVENT_READ,EVENT_WRITE
date=None:回调函数,这里只是提供回调函数名,不能直接执行,需要程序员自己完成,一般需要提供一个loop函数进行事件循环再调用
import socket
from urllib.parse import urlparse
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
selector = DefaultSelector()
urls = []
stop = False
class Fetcher:
def get_url(self, url):
# 解析url
url = urlparse(url)
self.host = url.netloc
self.path = url.path
self.data = b""
if self.path == "":
self.path = "/"
# 建立socket连接
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 非阻塞IO - 创建连接后不等待连接成功直接返回
self.client.setblocking(False)
try:
self.client.connect((self.host, 80))
except BlockingIOError as e:
pass
# 注册socket文件对象监听其IO状态
# 监听self.client.fileno()对应的`writefds`文件描述符,设定事件成功后要执行的回调函数
selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
def connected(self, key):
"""连接成功回调函数args:key:key为与给定文件描述符关联的注册密钥"""
# 调用这个回调函数,注销掉这个已经注册的文件
selector.unregister(key.fd)
# 此时socket已经是可写状态了,不用try...except异常,使用send()发送请求
self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
# 注册socket文件对象监听其IO状态
# 监听self.client.fileno()对应的`readfds`文件描述符,并设定事件成功后要执行的回调函数
selector.register(self.client.fileno(), EVENT_READ, self.readable)
def readable(self, key):
"""读取数据回调函数args:key:key为与给定文件描述符关联的注册密钥"""
# 接收数据
d = self.client.recv(1024)
if d:
self.data += d
else:
# 将所有数据读取完成注销文件描述符
selector.unregister(key.fd)
data = self.data.decode("utf8")
html_data = data.split("\r\n\r\n")[1]
print(html_data)
self.client.close()
def loop():
#事件循环,不停的请求socket的状态并调用对应的回调函数
while not stop:
# `select`or.`select`()返回已经准备好的数据报,一个元祖列表[(key, `event`s & key.`event`s), ]
ready = selector.select()
for key, mask in ready:
# key为与给定文件描述符关联的注册密钥。
# 提取回调函数
call_back = key.data
# 执行回调函数
call_back(key)
if __name__ == "__main__":
for url in range(20):
url = "http://shop.projectsedu.com/goods/{}/".format(url)
urls.append(url)
fetcher = Fetcher()
fetcher.get_url(url)
# 事件循环
loop()
整个过程可以这样简单描述:使用非阻塞给每个url创建一个socket套接字
select监听套接字,将套接字的“文件描述符”、“要监听的事件”、“IO状态准备好后要执行的回调函数”这三项注册到select中,得到一个key
然后开启事件循环,等待select返回
select返回ready对象,一个存放元祖的list,只要有状态就绪,事件循环函数就从ready中获取状态就绪的元祖,执行key中携带的回调函数,然后将这个key从ready中移除。
3.2 回调函数的缺点回调模式编码复杂度高不易维护:使用回调函数,是在单线程中实现并发,但是程序的逻辑不再是自上而下,而是分布在各个回调函数中,让程序的可读性很差。
异常处理困难,回调函数由事件循环调用,主逻辑函数将文件描述符注册后就不再处理以后的逻辑,所以异常不能直接由逻辑函数抛出,回调函数的异常就会很难处理。
多层回调
如果有数据需要被每个回调都处理,因为回调函数的存在问题就会变得很复杂
共享状态管理困难
为了兼具同步IO和异步IO的优点,引出了协程。
四、协程(Coroutine)
协程的出现是为了解决回调模式编码复杂度高
同步编程并发性低
多线程编程需要线程同步,即需要加入锁导致性能下降
的问题,而为了解决这些问题,我们需要采用同步的方式取编写异步的代码
使用单线程去切换任务线程是由操作系统负责切换的,使用单线程切换意味着我们需要程序员自己去做任务调度
单线程切换任务不需要加锁,就像函数之间的调用,并发性高,性能远高于线程切换
4.1 什么是协程
协程又叫微线程,是在单线程内完成子程序的切换,在子程序内部可中断,转而执行别的子程序,在适当的时候再返回来接着执行,子程序是“可以暂停的函数”。 协程的调度依然是“事件循环+协程模式”,
4.2 生成器
4.2.1 next & send
python对协程的支持是通过生成器实现的,生成器有两个作用:产出值,通过对生成器对象执行next()方法
传递值,通过对生成器对象执行send()方法,将值传递进生成器内部,同时重启生成器从上一次yield处继续向下执行,执行到下一个yield的位置。send()兼具传值和next()方法。
创建生成器后生成器还没有执行,是在第一次调用next()或者send(None)才开始执行到yield处,所以若使用send()传值,需要生成器已经启动过,即已经执行到一个yield位置才可以传递非None的值。
send()实现了传值给上一个yield和返回下一个yield的值。
4.2.2 close生成器调用close()方法会抛出GeneratorExit异常,前提是生成器已启动,若没有启动过则不会抛出异常。
GeneratorExit异常的产生意味着生成器对象的生命周期已经结束。因此,一旦产生了GeneratorExit异常,生成器无法进行迭代,再次使用next()方法会抛出StopIteration异常。后续执行的语句中不能再有yield语句,否则会产生RuntimeError异常。
GeneratorExit异常不能通过Exception异常捕获,因为Exception是常规异常的基类,GeneratorExit非常规异常,可以通过所有异常的基类BaseException来捕获。
4.2.3 throw
generator.throw(self, typ: Type[BaseException], val: Optional[BaseException] = ..., tb: Optional[TracebackType] = ...)主程序向生成器对象在上次yield处,抛出一个异常。生成器内部可以捕捉这条语句的异常。
然后会继续执行生成器对象中后面的语句,返回下一个yield语句的返回值。如果在生成器对象方法执行完毕后,依然没有遇到yield语句,抛出StopIteration异常。
也就是说throw实现了抛出上一个yield的异常和返回下一个yield的值的功能。异常在主程序抛出,在生成器中捕获。
def gen_func():
try:
yield 1
except Exception as e:
print(e)
yield 2
yield 3
if __name__ == '__main__':
gen = gen_func()
print(next(gen))
print(gen.throw(Exception, "generator error"))
print(next(gen))
1
generator error
2
3
4.2.4 yield from
python3.3新加入了yield from语法
遍历输出可迭代对象的元素
遍历输出可迭代对象是yield from的基础用法,yield from可以将此语句后面的可迭代对象的所有元素遍历输出。
我们用itertools模块中的chain方法来分析yield from 和yield的区别
_list=[1, 2, 3]
_dict={"name": "chovy", "age": 18}
_gen=(i for i in range(4,8))
for value in chain(_str, _list, _dict, _gen):
print(value)
'A' 'B' 'C' 1 2 3 'name' 'age' 4 5 6 7
我们先用yield自己模拟chain方法:
_str='ABC'
_list=[1, 2, 3]
_dict={"name": "chovy", "age": 18}
_gen=(i for i in range(4,8))
def _chain(*args, **kwargs):
for iterable in args:
for value in _iterable:
yield value
for value in _chain(_str, _list, _dict, _gen):
print(value)
'A' 'B' 'C' 1 2 3 'name' 'age' 4 5 6 7
再使用yield from:
_str='ABC'
_list=[1, 2, 3]
_dict={"name": "chovy", "age": 18}
_gen=(i for i in range(4,8))
def _chain(*args, **kwargs):
for iterable in args:
yield from iterable
for value in _chain(_str, _list, _dict, _gen):
print(value)
'A' 'B' 'C' 1 2 3 'name' 'age' 4 5 6 7
我们可以简单的认为,
def gen_func(iterable):
# ...
for i in iterable:
yield i
等价于
def gen_func(iterable):
#...
yield from iterable
我们发现,和yield不同的是,yield from后面跟的是一个可迭代对象,当然这个可迭代对象可以是生成器,因为协程就是基于生成器完成的,所以我们可以在一个生成器中通过yield from调用一个生成器,也就让我们在一个协程中调用另一个协程成为可能。
生成器的嵌套
我们用yield from一个生成器函数,实现生成器的嵌套:
# 子生成器
def woker():
sum_num = 0
count = 0
average = 0
while True:
join_num = yield average
if join_num is None:
break
count += 1
sum_num += join_num
average = sum_num/count
return sum_num, count, average
# 委托生成器
def delegate():
while True:
sum_num, count, average = yield from woker()
print("Calculation complete.\n计算个数 {} 个, 总和:{},平均数:{}".format(count, sum_num, average))
# 调用方
def client():
gen = delegate()
gen.send(None) # 预激生成器
print(gen.send(2))
print(gen.send(4))
print(gen.send(6))
if __name__ == '__main__':
main()
三个概念,调用方:调用委托生成器的客户端函数
委托生成器:包含yield from表达式的生成器函数
子生成器:yield from调用的生成器,用来生成数据并返回的实际工作函数
其中委托生成器的作用是在调用方和子生成器之间建立一个双向通道,即调用方可以通过send()方法直接传递数据到子生成器中,子生成器yield产生的值可以直接返回给调用方。
而委托生成器只起到了创建通道的作用,只有子生成器终止迭代,抛出StopIteration异常,其return的值才会返回给委托生成器。
yield from功能详解
从官方文档摘录对yield from的说明代码,对其功能进行剖析。
子生成器是一个生成器:
"""_i:子生成器,同时也是一个迭代器_y:子生成器生产的值_r:yield from 表达式最终的值_s:调用方通过send()发送的值_e:异常对象"""
#RESULE = yield from EXPR
_i = iter(EXPR) # 子生成器的函数名传入iter()中,生成子生成器_i
try:
_y = next(_i) # 预激生成器,yield的值赋给_y
except StopIteration as _e:
_r = _e.value # 抛出StopIteration异常后,会自动处理异常,把异常对象的值传递给yield from 表达式最终的值
else:
while 1: # 如果没有抛异常就不断循环
_s = yield _y # 生成器生产值,并等待调用方send()传入的值
try:
_y = _i.send(_s) # 接收到调用方传过来的值_s后,子生成器转发_s给自己并生产一个值_y
except StopIteration as _e:
_r = _e.value
break
RESULE = _r
以上是对子生成器是生成器,抛出StopIteration异常的描述,其实还有其他异常情况也需要在yield from中处理,比如说子生成器只是一个迭代器而非生成器时,不支持.throw()和.close()方法时;
如果子生成器支持.throw()和.close()方法,但是在子生成器内部,这两个方法都会抛出异常时;
调用方让子生成器自己抛出异常时;
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e: # 生成器对象被销毁时,会抛出一个GeneratorExit异常
try:
_m = _i.close # 首先获取close()方法
except AttributeError: # 如果子生成器只是迭代器,就没有close()方法,抛出AttributeError异常,然后忽略掉这个异常
pass
else:
_m() # 如果有close方法则执行_m()
raise _e # 抛出这异常_e
except BaseException as _e: # 遇到除GeneratorExit外的其他异常
_x = sys.exc_info() #
try:
_m = _i.throw # 首先获取throw()方法
except AttributeError: # 如果子生成器只是迭代器,就没有throw()方法,抛出AttributeError异常,然后raise _e
raise _e
else:
try:
_y = _m(*_x) # 如果有throw()方法,就调用_m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i) # 如果send值是None,则调用迭代器next()方法;
else:
_y = _i.send(_s) # 如果不为None,则调用迭代器的send()方法。
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
yield from要点总结子生成器产生的值直接返还给调用方,调用方send()传递的值都是直接给子生成器的。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。
子生成器退出时的return EXPR语句,会触发StopIteration异常。如果对子生成器的调用产生StopIteration异常,则子生成器StopIteration 异常中的第一个参数返回给yield from,然后委托生成器继续执行后面的语句,其他的异常会向上冒泡。
子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
传入委托生成器的异常里,除了GeneratorExit异常,其他的异常将会被传递到迭代器的throw()方法。如果调用throw()方法时出现了StopIteration异常,委托生成器会恢复运行,其他的异常会向上冒泡。
如果在委托生成器上调用.close()或者使用Ctrl+C等方法传入GeneratorExit异常,会调用子生成器的close()方法,没有的话就pass。如果在调用close()方法抛出了异常,就向上冒泡,否则委托生成器会抛出GeneratorExit异常。
可见,遍历输出可迭代对象的元素是yield from的最基础的功能,更强大的功能是yield from处理了几乎所有的异常,不用我们手动捕获和处理。
4.3 原生协程 async & await
python为了将协程和生成器的使用场景区分,使语义更加明确,加入async和await关键词用于定义原生协程。async代替了asyncio提供的@asyncio.coroutine,把一个生成器标记为协程,
await代替yield from,只能接收awaitable对象