上一篇说的是,通过yield 和 send 关键字,在需要的时候,指定代码的执行顺序,要实现asyncio的类似效果,我们还需要解决两个问题:

  1. 一个是在需要的时候,让出cpu,这里说的让出cpu,是指不让cpu将时间花费在等待io上, 而是去执行其他代码段。
  2. 另一个是,保证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()

以上就是我对python 中 ** yield ** 的一些理解,有不正确的地方,欢迎指正。