上一篇说的是,通过yield 和 send 关键字,在需要的时候,指定代码的执行顺序,要实现asyncio的类似效果,我们还需要解决两个问题:
- 一个是在需要的时候,让出cpu,这里说的让出cpu,是指不让cpu将时间花费在等待io上, 而是去执行其他代码段。
- 另一个是,保证io准备好时,可以回到之前的位置继续执行。
这两个问题的解决都比较依赖操作系统的实现,在python层只是对系统函数做了一层包装。
先说第一个,我们需要在io未完成时让出cpu,我们可以以非阻塞的方式进行io。
举个例子,在我们使用socket通信时,如果我们以阻塞的方式recv 数据,cpu(分给本进程的)会一直等待,直到有数据返回,才会接着执行我们的代码,在等待的过程中,cpu其实是浪费的,
而如果我们使用非阻塞的方式执行recv,系统会先看一下我们 recv 指定的缓存中是否有数据,如果没有,会调度cpu执行其他任务,当网卡在完成写入DMA后,通知cpu进行中断,并通知我们
或切换到我们注册的回调函数继续执行,这样在等待数据的过程中,cpu是被利用起来的。
在python 中,将socket 从同步模式改为异步,只需增加一个参数即可:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((remote_ip, remote_port))
# 阻塞模式
sock.recv(1024)
# 非阻塞
sock.recv(1024, socket.MSG_DONTWAIT)
第二,我们需要保证io完成时,能找到当时跳出的位置。
这个也好实现,我们只要用个dict,将每个关注的socket与该socket所在的生成器的对应关系保存起来就行,这样在操作系统返回关注的socket时,我们直接获取对应的生成器就行,不清楚
的可以直接去看最后代码中的read_socks 和 write_socks。
另外,还得说一下 select 这个函数。
select 函数是一个系统函数,也是执行异步io的关键所在,当我们需要实时关注某个socket io的变化时,可以将socket注册到select 函数上,它会帮我们监控我们的socket,并在有io完成(可读,可写,或错误)时,返回这些socket。python中select的简单用法可以看下面这歌代码片段:
import select
while 1:
# 如果传入的sockets有变化,会返回
readalbe, writable, _ = select.select(
read_socks.keys(), # 需要监视的 可读 sockets
[], # 需要监视的 可写 sockets
[] # 需要监视的 error sockets
)
好了,至此,我们所有的前期工作已经完成,下面来实现一个利用协程,异步发送http请求的代码:
import select
import time
import socket
def get_url(remote_ip, remote_port, url):
"""
:param remote_ip:
:param remote_port:
:param url:
:return:
"""
tmp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tmp_sock.connect((remote_ip, remote_port))
# 复制一个接受数据的socket
rd_sock = tmp_sock.dup()
try:
# 写入缓冲区后返回,这里注意一下,如果缓冲区大小 小于 要发送的消息的大小,就只能发送缓存大小的消息
tmp_sock.send(f"GET {url} HTTP/1.1\r\n\r\n".encode("utf-8"), socket.MSG_DONTWAIT)
# 发送完毕,关闭socket
tmp_sock.close()
except BlockingIOError as e:
pass
# cpu 返回到主流程
yield rd_sock
# 读数据
res = ""
while 1:
try:
# 非阻塞收取,返回到这里时,buffer中肯定是有数据的
tmp_res = rd_sock.recv(10240, socket.MSG_DONTWAIT)
res += tmp_res.decode("utf-8")
except BlockingIOError as e:
# 读完了, 关闭socket
rd_sock.close()
read_socks.pop(rd_sock, None)
break
return res
def get_all_urls(urls):
"""
:param urls:
:return:
"""
# 创建任务,并不会触发
tasks = [get_url(u_h, u_p, url) for (u_h, u_p, url) in urls]
# 触发任务
for t in tasks:
# 触发任务并获取响应socket
rd_socket = t.send(None)
# 注册要关注的socket 与 对应的上下文关系
read_socks.update({rd_socket: t})
# 轮训可读socket,有可读时,触发对应上下文
result = []
while read_socks:
reads, writes, _ = select.select(read_socks.keys(), [], [])
# 如果有可读socket
if reads:
for r in reads:
f = read_socks.get(r)
# 这里是r 可读后,控制cpu 去执行生成器 f yield 后的代码
res = yield from f
result.append(res)
yield result
def main():
# urls
targets = [
("182.61.200.6", 80, "/"),
("182.61.200.7", 80, "/"),
]
resp = get_all_urls(targets)
for i in resp:
print(i)
if __name__ == '__main__':
read_socks = {}
write_socks = {}
main()