Python 异步 IO 、协程、asyncio、async/await、aiohttp


From :廖雪峰 异步IO :https://www.liaoxuefeng.com/wiki/1016959663602400/1017959540289152

Python Async/Await入门指南 :https://zhuanlan.zhihu.com/p/27258289

Python 生成器 和 yield 关键字:https://blog.csdn.net/freeking101/article/details/51126293

协程与任务 官网文档https://docs.python.org/zh-cn/3/library/asyncio-task.html

Python中异步协程的使用方法介绍https://blog.csdn.net/freeking101/article/details/88119858

python 协程详解及I/O多路复用,I/O异步:https://blog.csdn.net/u014028063/article/details/81408395

Python协程深入理解:https://www.cnblogs.com/zhaof/p/7631851.html

asyncio 进阶:Python黑魔法 --- 异步IO( asyncio) 协程:https://www.cnblogs.com/dhcn/p/9033628.html

谈谈Python协程技术的演进:https://www.freebuf.com/company-information/153421.html

最后推荐一下《流畅的Python》,这本书中 第16章 协程的部分介绍的非常详细
《流畅的Python》pdf 下载地址:https://download.csdn.net/download/freeking101/10993120

gevent 是 python 的一个并发框架,以微线程 greenlet 为核心,使用了 epoll 事件监听机制以及诸多其他优化而变得高效。

aiohttp 使用代理 ip 访问 https 网站报错的问题https://blog.csdn.net/qq_43210211/article/details/108379917

Python:使用 Future、asyncio 处理并发

https://blog.csdn.net/sinat_38682860/article/details/105419842

异步  IO

在 IO 编程( 廖雪峰 Python IO 编程 :https://www.liaoxuefeng.com/wiki/1016959663602400/1017606916795776) 一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。

在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。

因为一个 IO 操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。

多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。

由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。

另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

消息模型 其实早在应用在桌面应用程序中了。一个 GUI 程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。

由于GUI 线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。

消息模型 是 如何解决 同步IO 必须等待IO操作这一问题的呢 ?

在消息处理过程中,当遇到 IO 操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。

在 “发出IO请求” 到收到 “IO完成” 的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

协程 (Coroutines)

在学习异步IO模型前,我们先来了解协程,协程 又称 微线程,纤程,英文名 Coroutine

子程序( 又叫 函数 ) 协程

  • 子程序 在 所有语言中都是层级调用。比如: A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。所以 子程序 即 函数 的调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。
  • 协程的调用 和 子程序 不同。协程 看上去也是 子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个 子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如:子程序 A 和 B :

def A():
    print('1')
    print('2')
    print('3')


def B():
    print('x')
    print('y')
    print('z')

假设由协程执行,在执行 A 的过程中,可以随时中断,去执行 B,B 也可能在执行过程中中断再去执行 A,结果可能是:

1
2
x
y
3
z

但是在 A 中是没有调用 B 的,所以 协程的调用 比 函数调用 理解起来要难一些。

看起来 A、B 的执行有点像多线程,但 协程 的特点在于是一个线程执行。

协程 和 多线程比,协程有何优势?

  • 1. 最大的优势就是协程极高的执行效率。因为 子程序 切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 2. 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?

最简单的方法是 多进程 + 协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python 对 协程 的支持 是通过 generator (生成器)实现的

在 generator 中,我们不但可以通过 for 循环来迭代,还可以不断调用 next() 函数获取由 yield 语句返回的下一个值。

但是 Python 的 yield 不但可以返回一个值,它还可以接收调用者发出的参数。

来看例子:

传统的 生产者-消费者 模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author      : 
# @File        : text.py
# @Software    : PyCharm
# @description : XXX


def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'


def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()


c = consumer()
produce(c)

执行结果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到 consumer函数 是一个 generator,把一个 consumer 传入 produce 后:

  1. 首先调用 c.send(None) 启动生成器;
  2. 然后,一旦生产了东西,通过 c.send(n) 切换到 consumer 执行;
  3. consumer 通过 yield拿到消息,处理,又通过yield把结果传回;

  4. produce 拿到 consumer 处理的结果,继续生产下一条消息;

  5. produce 决定不生产了,通过 c.close() 关闭 consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce 和 consumer 协作完成任务,所以称为 “协程”,而非线程的抢占式多任务。

最后套用 Donald Knuth 的一句话总结协程的特点:“子程序就是协程的一种特例。”

参考源码:https://github.com/michaelliao/learn-python3/blob/master/samples/async/coroutine.py

在 Python 中,异步函数  通常 被称作  协程

创建一个协程仅仅只需使用 async 关键字,或者使用 @asyncio.coroutine 装饰器。下面的任一代码,都可以作为协程工作,形式上也是等同的:

import asyncio

# 方式 1
async def ping_server(ip):
        pass


# 方式 2
@asyncio.coroutine
def load_file(path):
      pass

上面这两个 特殊的函数,在调用时会返回协程对象。熟悉 JavaScript 中 Promise 的同学,可以把这个返回对象当作跟 Promise 差不多。调用他们中的任意一个,实际上并未立即运行,而是返回一个协程对象,然后将其传递到 Eventloop 中,之后再执行。

  • 如何判断一个 函数是不是协程 ?   asyncio 提供了 asyncio.iscoroutinefunction(func) 方法。
  • 如何判断一个 函数返回的是不是协程对象 ?  可以使用 asyncio.iscoroutine(obj) 。

用 asyncio 提供的 @asyncio.coroutine 可以把一个 generator 标记为 coroutine 类型,然后在 coroutine 内部用 yield from 调用另一个 coroutine 实现异步操作。

Python 3.5 开始引入了新的语法 async await

为了简化并更好地标识异步 IO,从 Python 3.5 开始引入了新的语法 async await,可以让 coroutine 的代码更简洁易读。

 async / await 是 python3.5 的新语法,需使用 Python3.5 版本 或 以上才能正确运行。

注意:async 和 await 是针对 coroutine 的新语法,要使用新的语法,只需要做两步简单的替换:

  • 把 @asyncio.coroutine 替换为 async 
  • 把 yield from 替换为 await

 Python 3.5 以前 版本原来老的语法使用 协程

import asyncio


@asyncio.coroutine
def hello():
    print("Hello world!")
    r = yield from asyncio.sleep(1)
    print("Hello again!")

Python 3.5 以后 用新语法重新编写如下:

import asyncio


async def hello():
    print("Hello world!")
    r = await asyncio.sleep(1)
    print("Hello again!")

在过去几年内,异步编程由于某些好的原因得到了充分的重视。虽然它比线性编程难一点,但是效率相对来说也是更高。

比如,利用 Python 的 异步协程 (async coroutine) ,在提交 HTTP 请求后,就没必要等待请求完成再进一步操作,而是可以一边等着请求完成,一边做着其他工作。这可能在逻辑上需要多些思考来保证程序正确运行,但是好处是可以利用更少的资源做更多的事。

即便逻辑上需要多些思考,但实际上在 Python 语言中,异步编程的语法和执行并不难。跟 Javascript 不一样,现在 Python 的异步协程已经执行得相当好了。

对于服务端编程,异步性似乎是 Node.js 流行的一大原因。我们写的很多代码,特别是那些诸如网站之类的高 I/O 应用,都依赖于外部资源。这可以是任何资源,包括从远程数据库调用到 POST 一个 REST 请求。一旦你请求这些资源的任一一个,你的代码在等待资源响应时便无事可做 (译者注:如果没有异步编程的话)。

有了异步编程,在等待这些资源响应的过程中,你的代码便可以去处理其他的任务。

Python async / await 手册

Python 部落:Python async/await 手册:https://python.freelycode.com/contribution/detail/57

知乎:从 0 到 1,Python 异步编程的演进之路( 通过爬虫演示进化之路 )https://zhuanlan.zhihu.com/p/25228075

async / await 的使用

async 用来声明一个函数是协程然后使用 await 调用这个协程, await 必须在函数内部,这个函数通常也被声明为另一个协程await 的目的是等待协程控制流的返回yield 的目的 是 暂停并挂起函数的操作。

正常的函数在执行时是不会中断的,所以你要写一个能够中断的函数,就需要添加 async 关键。

  • async 用来声明一个函数为异步函数异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行。
  • await 可以将耗时等待的操作挂起,让出控制权await 语法来挂起自身的协程 )比如:异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。await 后面只能跟 异步程序 或 有 __await__ 属性 的 对象因为异步程序与一般程序不同

假设有两个异步函数 async a,async b,a 中的某一步有 await,当程序碰到关键字 await b() 后,异步程序挂起后去执行另一个异步b程序,就是从函数内部跳出去执行其他函数,当挂起条件消失后,不管b是否执行完,要马上从b程序中跳出来,回到原程序执行原来的操作。如果 await 后面跟的 b 函数不是异步函数,那么操作就只能等 b 执行完再返回,无法在 b 执行的过程中返回。如果要在 b 执行完才返回,也就不需要用 await 关键字了,直接调用 b 函数就行。所以这就需要 await 后面跟的是 异步函数了。在一个异步函数中,可以不止一次挂起,也就是可以用多个 await 。

看下 Python 中常见的几种函数形式:

# 1. 普通函数
def function():
    return 1
    
# 2. 生成器函数
def generator():
    yield 1

# 在3.5过后,我们可以使用async修饰将普通函数和生成器函数包装成异步函数和异步生成器。

# 3. 异步函数(协程)
async def async_function():
    return 1

# 4. 异步生成器
async def async_generator():
    yield 1

通过类型判断可以验证函数的类型

import types


# 1. 普通函数
def function():
    return 1
    
# 2. 生成器函数
def generator():
    yield 1

# 在3.5过后,我们可以使用async修饰将普通函数和生成器函数包装成异步函数和异步生成器。

# 3. 异步函数(协程)
async def async_function():
    return 1

# 4. 异步生成器
async def async_generator():
    yield 1


print(type(function) is types.FunctionType)
print(type(generator()) is types.GeneratorType)
print(type(async_function()) is types.CoroutineType)
print(type(async_generator()) is types.AsyncGeneratorType)

直接调用异步函数不会返回结果,而是返回一个coroutine对象:

print(async_function())
# <coroutine object async_function at 0x102ff67d8>

协程 需要通过其他方式来驱动,因此可以使用这个协程对象的 send 方法给协程发送一个值:

print(async_function().send(None))

不幸的是,如果通过上面的调用会抛出一个异常:StopIteration: 1

因为 生成器 / 协程 在正常返回退出时会抛出一个 StopIteration 异常,而原来的返回值会存放在 StopIteration 对象的 value 属性中,通过以下捕获可以获取协程真正的返回值: 

try:
    async_function().send(None)
except StopIteration as e:
    print(e.value)
# 1

通过上面的方式来新建一个 run 函数来驱动协程函数,在协程函数中,可以通过 await 语法来挂起自身的协程,并等待另一个 协程 完成直到返回结果:

def run(coroutine):
    try:
        coroutine.send(None)
    except StopIteration as e:
        return 'run() : return {0}'.format(e.value)

async def async_function():
    return 1


async def await_coroutine():
    result = await async_function()
    print('await_coroutine() : print {0} '.format(result))

ret_val = run(await_coroutine())
print(ret_val)

要注意的是,await 语法只能出现在通过 async 修饰的函数中,否则会报 SyntaxError 错误。

而且 await 后面的对象需要是一个 Awaitable,或者实现了相关的协议。

查看 Awaitable 抽象类的代码,表明了只要一个类实现了__await__方法,那么通过它构造出来的实例就是一个 Awaitable:

class Awaitable(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __await__(self):
        yield

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Awaitable:
            return _check_methods(C, "__await__")
        return NotImplemented

而且可以看到,Coroutine类 也继承了 Awaitable,而且实现了 send,throw 和 close 方法。所以 await 一个调用异步函数返回的协程对象是合法的。

class Coroutine(Awaitable):
    __slots__ = ()

    @abstractmethod
    def send(self, value):
        ...

    @abstractmethod
    def throw(self, typ, val=None, tb=None):
        ...

    def close(self):
        ...
        
    @classmethod
    def __subclasshook__(cls, C):
        if cls is Coroutine:
            return _check_methods(C, '__await__', 'send', 'throw', 'close')
        return NotImplemented

接下来是异步生成器,来看一个例子:

假如我要到一家超市去购买土豆,而超市货架上的土豆数量是有限的:

class Potato:
    @classmethod
    def make(cls, num, *args, **kws):
        potatos = []
        for i in range(num):
            potatos.append(cls.__new__(cls, *args, **kws))
        return potatos

all_potatos = Potato.make(5)

现在我想要买50个土豆,每次从货架上拿走一个土豆放到篮子:

def take_potatos(num):
    count = 0
    while True:
        if len(all_potatos) == 0:
            sleep(.1)
        else:
            potato = all_potatos.pop()
            yield potato
            count += 1
            if count == num:
                break

def buy_potatos():
    bucket = []
    for p in take_potatos(50):
        bucket.append(p)

对应到代码中,就是迭代一个生成器的模型,显然,当货架上的土豆不够的时候,这时只能够死等,而且在上面例子中等多长时间都不会有结果(因为一切都是同步的),也许可以用多进程和多线程解决,而在现实生活中,更应该像是这样的:

import asyncio
import random


class Potato:
    @classmethod
    def make(cls, num, *args, **kws):
        potatos = []
        for i in range(num):
            potatos.append(cls.__new__(cls, *args, **kws))
        return potatos


all_potatos = Potato.make(5)


async def take_potatos(num):
    count = 0
    while True:
        if len(all_potatos) == 0:
            await ask_for_potato()
        potato = all_potatos.pop()
        yield potato
        count += 1
        if count =&
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值