Python协程和异步IO

一、并发、并行、同步、异步、阻塞、非阻塞

并发: 一个时间段内,有几个程序在同一个CPU上运行,但是任意时刻只有一个程序在CPU上运行。
并行: 任何时间点,有多个程序运行在多个CPU上(最多和CPU数量一致)。
同步: 是指代码调用IO操作时,必须等待IO操作完成才能返回的调用方式。
异步: 是指代码调用IO操作时,不必等待IO操作完成就能返回的调用方式。
阻塞: 调用函数的时候当前线程被挂起。
非阻塞: 是指调用函数的时候当前线程不会被挂起,而是立即返回。

二、五种I/O模型

在这里插入图片描述
在这里插入图片描述
阻塞I式/O:系统调用不会立即返回结果,当前线程会阻塞,等到获得结果或报错时在返回(问题:如在调用send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。)

在这里插入图片描述
非阻塞式I/O:调用后立即返回结果(问题:不一定三次握手成功,recv() 会被循环调用,循环调用recv()将大幅度推高CPU 占用率),做计算任务或者再次发起其他连接就较有优势
在这里插入图片描述
在这里插入图片描述
I/O复用:它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。(阻塞式的方法,可以监听多个socket状态)(问题:将数据从内核复制到用户空间的时间不能省)
在这里插入图片描述
信号驱动式I/O:运用较少
在这里插入图片描述
异步I/O:它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

三、C10K问题和解决方式

谓c10k问题,指的是服务器同时支持成千上万个客户端的问题,也就是concurrent 10 000 connection(这也是c10k这个名字的由来)。由于硬件成本的大幅度降低和硬件技术的进步,如果一台服务器同时能够服务更多的客户端,那么也就意味着服务每一个客户端的成本大幅度降低,从这个角度来看,问题显得非常有意义。

3.1 解决方法

每个线程/进程处理一个连接:

但是由于申请进程/线程会占用相当可观的系统资源,同时对于多进程/线程的管理会对系统造成压力,因此这种方案不具备良好的可扩展性。因此,这一思路在服务器资源还没有富裕到足够程度的时候,是不可行的;即便资源足够富裕,效率也不够高。

问题:资源占用过多,可扩展性差

每个进程/线程同时处理多个连接(IO多路复用):

最简单的方法是循环挨个处理各个连接,每个连接对应一个 socket,当所有 socket 都有数据的时候,这种方法是可行的。但是当应用读取某个 socket 的文件数据不 ready 的时候,整个应用会阻塞在这里等待该文件句柄,即使别的文件句柄 ready,也无法往下处理。

直接循环处理多个连接。问题:任一文件句柄的不成功会阻塞住整个应用。

epoll+回调+事件循环方式url

1.通过非阻塞I/O实现http请求:

import socket
from urllib.parse import urlparse

def get_url(url):
    #通过socket请求html
    url=urlparse(url)
    host=url.netloc
    path=url.path
    if path=="":
        path="/"
    #建立socket连接
    client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    #设置成非阻塞(抛异常:BlockingIOError: [WinError 10035] 无法立即完成一个非阻止性套接字操作。)
    client.setblocking(False)
    try:
        client.connect((host,80))
    except BlockingIOError as e:
        pass
    #向服务器发送数据(还未连接会抛异常)
    while True:
        try:
            client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
            break
        except OSError as e:
            pass
    #将数据读取完
    data=b""
    while True:
        try:
            d=client.recv(1024)
        except BlockingIOError as e:
            continue
        if d:
            data+=d
        else:
            break
    #会将header信息作为返回字符串
    data=data.decode('utf8')
    print(data.split('\r\n\r\n')[1])
    client.close()

if __name__=='__main__':
    get_url('http://www.baidu.com')

2.使用select完成http请求(循环回调):
优点:并发性高(驱动整个程序主要是回调循环loop(),不会等待,请求操作系统有什么准备好了,准备好了就执行【没有线程切换等,只有一个线程,当一个url连接建立完成后就会注册,然后回调执行】,省去了线程切换和内存)

#自动根据环境选择poll和epoll
from selectors import DefaultSelector,EVENT_READ,EVENT_WRITE
selector=DefaultSelector()
urls=[]
#全局变量
stop=False
class Fetcher:
    def connected(self, key):
        #取消注册
        selector.unregister(key.fd)
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
        selector.register(self.client.fileno(),EVENT_READ,self.readable)

    def readable(self,key):
        d = self.client.recv(1024)
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)
            # 会将header信息作为返回字符串
            data = self.data.decode('utf8')
            print(data.split('\r\n\r\n')[1])
            self.client.close()
            urls.remove(self.spider_url)
            if not urls:
                global stop
                stop=True

    def get_url(self,url):
        self.spider_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)
        self.client.setblocking(False)
        try:
            self.client.connect((self.host, 80))
        except BlockingIOError as e:
            pass

        #注册写事件,及回调函数
        selector.register(self.client.fileno(),EVENT_WRITE,self.connected)

def loop():
    #回调+事件循环+select(poll/epoll)
    #事件循环,不停的调用socket的状态并调用对应的回调函数
    #判断哪个可读可写,select本身不支持register模式
    #socket状态变化后的回调使用程序员完成的
    if not stop:
        while True:
            ready=selector.select()
            for key,mask in ready:
                call_back=key.data
                call_back(key)


if __name__=='__main__':
    fetcher=Fetcher()
    fetcher.get_url('http://www.baidu.com')
    loop()

3.使用协程的方式:

  • 采用同步的方式去编写异步的代码
  • 采用单线程去解决任务:线程是由操作系统切换,单线程切换意味着需要我们自己去调度任务;不在需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高。

四、生成器的send和yield from

4.1 生成器send和next方法:

  • 启动生成器方式有两种:1.next();2.send();
  • 生成器可以产出值;也可以接收值(调用方传递进来的值)

send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield的位置(注:在调用send()发送非none之前,我们必须启动一次生成器,否则会抛错,方式有两种gen.send(None)或者next(gen))

4.2 close()方法:(关闭生成器)

  • 自己处理的话会抛异常,gen.close(),RuntimeError: generator ignored GeneratorExit,如果是except Exception就不会抛异常,GeneratorExit是继承至BaseException的,Exception也是继承于BaseException的
def gen_func():
    #自己处理的话会抛异常,gen.close(),RuntimeError: generator ignored GeneratorExit
    try:
        yield 'https://www.baidu.com'
    #如果是except Exception就不会抛异常,GeneratorExit是继承至BaseException的,Exception也是继承于BaseException的
    except GeneratorExit as e:
        pass
    yield 1
    yield 2
    return 'LYQ'

if __name__=='__main__':
    #抛异常StopIteration:
    gen=gen_func()
    print(next(gen))
    gen.close()
    print(next(gen))

4.3 throw()方法:向生成器中扔异常,需要自己处理,否则会抛错

def gen_func():
    try:
        yield 'https://www.baidu.com'
    except Exception:
        pass
    yield 1
    yield 2
    return 'LYQ'

if __name__=='__main__':
    #抛异常StopIteration:
    gen=gen_func()
    print(next(gen))
    #扔一个异常,是第一句的异常
    gen.throw(Exception,'download error')
    print(next(gen))
    #扔一个异常,是第二句的异常
    gen.throw(Exception,'download error')
    print(next(gen))

4.4 yield from

main调用方 g1:委托生成器 gen:子生成器:

def g1(gen):
    yield from gen
gen=range(10)
def main():
    g=g1(gen)
    #直接发送给子生成器
    print(g.send(None))
#main:调用方 g1:委托生成器 gen:子生成器
#yield from会在调用方与子生成器之间建立一个双向通道
main()

例子

final_result = {}


def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        x = yield
        print(pro_name + "销量: ", x)
        if not x:
            break
        total += x
        nums.append(x)
    #直接返回到yield from sales_sum(key)
    return total, nums


def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)
        print(key + "销量统计完成!!.")


def main():
    data_sets = {
        "面膜": [1200, 1500, 3000],
        "手机": [28, 55, 98, 108],
        "大衣": [280, 560, 778, 70],
    }
    for key, data_set in data_sets.items():
        print("start key:", key)
        m = middle(key)
        #直接send到子生成器里面(x = yield)
        m.send(None)  # 预激middle协程
        for value in data_set:
            m.send(value)  # 给协程传递每一组的值
        m.send(None)
    print("final_result:", final_result)


if __name__ == '__main__':
    main()

无yield from

def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        x = yield
        print(pro_name + "销量: ", x)
        if not x:
            break
        total += x
        nums.append(x)
    #直接返回到yield from sales_sum(key)
    return total, nums

if __name__ == "__main__":
    #直接与子生成器通信(没用yield from就需要捕获异常)
    my_gen = sales_sum("手机")
    my_gen.send(None)
    my_gen.send(1200)
    my_gen.send(1500)
    my_gen.send(3000)
    try:
        my_gen.send(None)
    #获取返回值
    except StopIteration as e:
        result = e.value
        print(result)

总结:

  • 子生成器生产的值,都是直接传给调用方的;调用方通过.send()发送的值都是直接传递给子生成器的;如果发送的是 None,会调用子生成器的__next__()方法,如果不是 None,会调用子生成器的.send()方法;
  • 子生成器退出的时候,最后的return EXPR,会触发一个StopIteration(EXPR)异常;
  • yield from表达式的值,是子生成器终止时,传递给StopIteration异常的第一个参数;
  • 如果调用的时候出现StopIteration异常,委托生成器会恢复运行,同时其他的异常会向上 “冒泡”;4
  • 传入委托生成器的异常里,除了GeneratorExit之外,其他的所有异常全部传递给子生成器的.throw()方法;如果调用.throw()的时候出现了StopIteration异常,那么就恢复委托生成器的运行,其他的异常全部向上 “冒泡”;
  • 如果在委托生成器上调用.close()或传入GeneratorExit异常,会调用子生成器的.close()方法,没有的话就不调用。如果在调用.close()的时候抛出了异常,那么就向上 “冒泡”,否则的话委托生成器会抛出GeneratorExit异常。

五. 生成器如何变成协程?

1.生成器可以暂停并获取状态:

#生成器是可以暂停的函数
import inspect
def gen():
    yield 1
    return True

if __name__=='__main__':
    g1=gen()
    #获取生成器状态 GEN_CREATED(创建)
    print(inspect.getgeneratorstate(g1))
    next(g1)
    #GEN_SUSPENDED暂停
    print(inspect.getgeneratorstate(g1))
    try:
        next(g1)
    except StopIteration:
        pass
    #GEN_CLOSED关闭
    print(inspect.getgeneratorstate(g1))

2.协程的调度依然是 事件循环+协程模式 ,协程是单线程模式:

#生成器是可以暂停的函数
import inspect
# def gen_func():
#     value=yield from
#     #第一返回值给调用方, 第二调用方通过send方式返回值给gen
#     return "bobby"
#1. 用同步的方式编写异步的代码, 在适当的时候暂停函数并在适当的时候启动函数
import socket
def get_socket_data():
    yield 1

def downloader(url):
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.setblocking(False)

    try:
        client.connect((host, 80))  # 阻塞不会消耗cpu
    except BlockingIOError as e:
        pass

    selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
    #如果get_socket_data()中出现异常,会直接抛给downloader(向上抛)
    source = yield from get_socket_data()
    data = source.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)

def download_html(html):
    html = yield from downloader()

if __name__ == "__main__":
    #协程的调度依然是 事件循环+协程模式 ,协程是单线程模式
    pass

六.async和await原生协程

1.python为了将语义变得更加明确,就引入了async和await关键字定义原生的协程:
生成器实现的协程又可以当生成器,又可以当协程,且代码凌乱,不利于后期维护。原生的协程中不可以yield,否则会抛错(让协程更加明确)
在这里插入图片描述
可异步调用:实际实现了__await__魔法函数

await:将控制权交出去并等待结果返回,await只能接收awaitable对象,可以理解成yield from

# from collections import Awaitable
#如果是函数,就要使用coroutine装饰器,实际将__await_指向___iter__
# import types
# @types.coroutine
# def downloader(url):
#     return "haha"

async def downloader(url):
    return "haha"
async def download_url(url):
    #将控制权交出去并等待结果返回,await只能接收awaitable对象,可以理解成yield from
    html=await downloader(url)
    return html

if __name__=='__main__':
    coro=download_url('www.baidu.com')
    #原生协程不能调用next
    coro.send(None)
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值