python3 asyncio

1、背景

        asyncio 库最早由PyPI 提供,在基于Python3.3的环境时就已经能够使用,而在2013年9月20日,正式加入Python3.4官方库大家族,使用coroutine装饰器与yield from配合实现函数的异步;一直到Python3.5才将许多临时API定格下来,虽然计划是这样的,其实到Python3.6才定格。Python3.6为其加入了async 和await 两大关键字,更加方便了实现异步;其中yield from与await的功能类似,都是将当前尚未实现完毕的程序挂起,交出CPU的控制权,等待程序执行完毕之后返回一个协程,能看出Python官方对其的重视程度。

        协程存在的价值简易概括:由之前的进程或者线程切换粒度变成了程序上下文交互,极大的节省了资源的开销,提升了并发量与资源利用率,打破了GIL(全局解释锁)同一时刻只能执行一个线程运行的限制。

        协程主要是为了解决IO阻塞问题,包括文件IO与网络IO。

  • asyncio.coroutine(func)

        是一个装饰器,定义一个协程,这个是python3.4中采用的方式,配合yield from使用,
        在python3.5中已经使用async和await替代,从而更方便协程的使用

  • asyncio.sleep(delay, result=None, *, loop=None)

       在协程中调用睡眠函数必须使用这个,而不能使用time中的sleep函数,使用方式:await asyncio.sleep(1)

  •  asyncio.gather(*coros_or_futures, loop=None, return_exceptions=False)

       将多个协程函数并发执行,但是其实也是有运行顺序的(从左到右)的,返回一个future aggregating results,也需要  注册到时间循环中才可以运行,如果传递task列表进入的话,必须前面加*

  • asyncio.get_event_loop()

       返回一个协程时间循环,如果有当前正在运行的事件循环,则返回当前正在运行的事件循环,如果没有的话,则重新创建一个

  • asyncio.set_event_loop(loop)

       将loop设置为当前的事件循环,loop用一个新的事件循环可以替换当前的事件循环

  • asyncio.Lock(loop=None)

       创建协程锁,loop用一个新的事件循环可以替换当前的事件循环

  • asyncio.Queue(maxsize=0, *, loop=None)

       创建协程队列,maxsize设定创建队列的大小,loop用一个新的事件循环可以替换当前的事件循环

  • asyncio.as_completed(fs, *, loop=None, timeout=None)

       返回一个协程迭代器,与并发包concurrent.futures.as_completed中的行为类似,result=await coroutine,loop用一个新的事件循环可以替换当前的事件循环

扫描二维码关注公众号,回复: 5895373 查看本文章

      for task in asyncio.as_completed(tasks):
            resp = await task

  • asyncio.wait(fs, *, loop=None, timeout=None, return_when=ALL_COMPLETED)

       协程的参数是一个由期物或协程构成的可迭代对象; wait 会分别把各个协程包装进一个 Task 对象; 功能是根据传递参数选择什么情况下返回已完成的协程,fs表示协程列表或者协程迭代器,loop用一个新的事件循环可以替换当前的事件循环
       asyncio.FIRST_COMPLETED:第一个协程完成的话,就结束所有协程的执行
       asyncio.FIRST_EXCEPTION:第一个协程产生异常的话,就结束所有协程的执行
       asyncio.ALL_COMPLETED:等所有协程完成的话,再返回所有协程的执行状态

  • asyncio.wait_for(fut, timeout, *, loop=None)

       传递一个单一的future或一个coroutine,timeout属于必传参数,返回执行结果

  • asyncio.get_event_loop_policy()

       与get_event_loop()的区别是:get_event_loop_policy直接创建一个事件循环,get_event_loop如果已经有当前的事件循环就返回,没有的话调用get_event_loop_policy创建

  • loop = asyncio.get_event_loop()

       创建一个事件循环对象

  • r = loop.run_until_complete(future)

       运行事件循环,直到future运行结束,future可以为协程对象也可以为tasks,如果不是task会自动进行转化为task再执行

  • loop.close()

       关闭当前事件循环

  • loop.create_future()

       利用事件循环创建一个future,可以理解为一个可以挂起当前的操作,让出操作权的将来要执行的对象

  • loop.run_in_executor(executor, func, *args)

       参数executor为一个异步线程池,func为执行的具体函数,args为func的执行需要传递参数;与tornado中的tornado.concurrent.run_on_executor有异曲同工之妙

  • loop.run_forever()

      会永远阻塞当前线程,直到有人停止了该 event loop 为止;所以在同一个线程里,两个 event loop 无法同时 run,但这不能阻止您用两个线程分别跑两个 event loop

  • asyncio.ensure_future(coro_or_future, *, loop=None)

      利用asyncio将一个异步函数注册为future对象,如果coro_or_future为future对象,会直接返回

  • lock = asyncio.Event()

      基于threading.Event来实现的,可以用用asyncio.Event做缓存等待,让当前事件循环处于阻塞的状态;Event中刚开始有个默认变量未False,是成为阻塞状态,调用lock.set()可以解锁,也就是改变默认变量为True,解除阻塞

  • asyncio.Future()

      在 asyncio 包中, BaseEventLoop.create_task(…) 方法接收一个协程,排定它的运行时间,然后返回一个 asyncio.Task 实例——也是 asyncio.Future 类的实例,因为 Task 是Future 的子类,用于包装协程。

  • asyncio.Semaphore(value=1, *, loop=None)

      用Semaphore做并发量控制,默认并发量为1,限制函数访问的最大并发量,比如访问redis或者数据库有最大连接对象的限制,可以使用Semaphore实现这个控制

  • future

      通常情况下自己不应该创建期物,而只能由异步并发框架(concurrent.futures 或 asyncio)实例化。原因很简单:期物表示终将发生的事情,而确定某件事会发生的唯一方式是执行的时间已经排定

2、code示例

简易创建一个协程

import asyncio
# async声明一个协程方法
async def execute(x):
    print('Number:', x)

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

# 创建事件循环
loop = asyncio.get_event_loop()
# 将coroutine注册到事件循环,实质run_until_complete将coroutine封装成一个task对象,再进行运行的
loop.run_until_complete(coroutine)
print('After calling loop')
# 注意在此处是无法打印coroutine的运行结果的

 利用事件循环显式创建task,但是必须先创建loop

import asyncio
async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:', task)
# 将task传入再进行运行
loop.run_until_complete(task)
# 利用task可以获取任务的执行结果,task.result()
print('Task:', task)
print('After calling loop')

 可以先创建task,再创建事件循环

import asyncio

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

task = asyncio.ensure_future(coroutine)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

绑定回调(利用task.add_done_callback给task绑定回调函数) 

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

def callback(task):
    print('Status:', task.result())

coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:', task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)

使用flask在本地搭建一个慢速服务器,模拟爬虫,以下多任务请求的时候可以调用,code如下

from flask import Flask
import time

app = Flask(__name__)

@app.route('/')
def index():
    time.sleep(5)
    return 'haha!'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=10001, debug=False, threaded=True)

1、协程多任务请求

使用请求时必须使用异步请求库aiohttp,使用requests无效,因为await后面不能跟requests的请求返回对象,await后面能跟随的只能为如下三种:

  • A native coroutine object returned from a native coroutine function,一个原生 coroutine 对象。

  • A generator-based coroutine object returned from a function decorated with types.coroutine(),一个由 types.coroutine() 修饰的生成器,这个生成器可以返回 coroutine 对象。

  • An object with an await__ method returning an iterator,一个包含 __await 方法的对象返回的一个迭代器。

如果将requests请求只用async封装成异步调用的函数,然后再用await去修饰可以吗?答案依旧是不可以。我们必须使用支持异步请求的操作方式,才能实现真正意义上的异步 

方式一:run_until_complete组合wait

import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    await session.close()  # 注意此处必须使用await,不然会报警告,session未关闭

    return result

async def request():
    url = 'http://127.0.0.1:10001/'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)

# 多任务执行之wait,ensure_future创建future对象
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

方式二:run_until_complete组合gather

import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    await session.close()

    return result


async def request():
    url = 'http://10.24.103.91:8802/'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)

futures = asyncio.gather(request(), request())
loop = asyncio.get_event_loop()
loop.run_until_complete(futures)

end = time.time()
print('Cost time:', end - start)

2、协程限制最大同时访问次数(例如:redis有最大连接限制,所以必须限制最大连接池,有的数据库也有最大连接限制)

用asyncio.Semaphore做并发量控制

import asyncio

class DataSource:
    def __init__(self, semaphore=3):
        self.semaphore = asyncio.Semaphore(semaphore)

    async def compute(self):
        print("compute start")
        # 获取锁
        await self.semaphore.acquire()
        print("get semaphore")
        await asyncio.sleep(3)  # 模拟耗费6秒IO操作
        # 释放锁
        self.semaphore.release()

        print("compute end and release semaphore")

async def main():
    print("main start")
    ds = DataSource()
    # 此处必须传递所有对象,直接传递对象的list会报错
    await asyncio.gather(*[ds.compute() for _ in range(6)])
    print("main end")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

程序执行结果

用asyncio.Event做缓存等待

作为一个数据聚合站点,很容易遇到数据源重复调用问题,而这类问题在协程框架下就能很easy的解决了。asyncio.Event与threading.Event效果类似,下面解释一下这样做的优点:

1. 由于协程结构,很有可能同时进行同样的IO调用,我希望只有一次IO操作,降低对方服务压力,可以用于设计API。

2. 每个数据源自我管理,控制重复访问和设置等待。

import asyncio

class DataSource:
    # 此处采用设计模式-享元模式,所有的DataSource对象都会共享属性lock与cache的值,可以对一些共同行为做控制
    lock = None
    cache = None  # 在此表示共同的任务执行返回结果

    async def compute(self):
        print("start")
        if self.lock is not None:
            await self.lock.wait()
            print("get cache!")
            return self.cache

        # 创建一个事件锁,Event创建对象时_value的值默认为False,当调用set时,会将其设为True
        self.lock = asyncio.Event()
        self.cache = await asyncio.sleep(6)  # 模拟 6s IO 操作,做等待
        # 释放事件锁,当调用set时,会将_value设为True
        self.lock.set()
        print("compute end")
        return self.cache

async def main():
    print("main start")
    ds = DataSource()
    await asyncio.gather(ds.compute(), ds.compute())
    print("main end")


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

3、使用小结

  • asyncio.gather与asyncio.wait
  1.  asyncio.gather是个high level API,而asyncio.wait是个low level API。
  2. gather偏向于结果,而wait偏向于过程控制。
  3. gather相当于把futures当成一个整体,当gather本身被cancel则所有的子futures也会被cancel,而当一个子future被cancel,gather并不会被cancel,而是认为子future抛出了一个CancelledError的报错。
  4. wait返回dones和pendings两个futures,函数本身可以精确控制过程timeout参数控制超时,return_when参数有三种模式FIRST_COMPLETED、FIRST_EXCEPTION、ALL_COMPLETED。可以对pending进行cancel,获取result则需要done.result。
  5. gather返回有序依据传入顺序而wait返回结果无序
  • loop. run_in_executor(executor, func, *args)
  1. 使用协程也有一定的缺点,就是不同的IO需要相应的async库支持,当遇到如sqlserver这类暂时没有协程库支持的数据源,则需要用run_in_executor包成协程。
  2. 一般来说run_in_executor就是用一个线程池来跑function,其结果return了一个future,executor参数就是需要将创建的线程池执行器传入,需要注意在创建的线程池执行器要指定最大线程数量,当调用run_in_executor达到最大数量时,后置的请求就会阻塞等待前面的释放。
  3. 此函数的使用与tornado.web.RequestHandler实现线程池协程有异曲同工之妙;tornado中需要设置executor为共享concurrent异步库中的线程池对象变量,使用tornado.concurrent.run_on_executor为装饰器,修饰函数,则会实现
  • yield from与 await 的异同
  1. 共同点:都是实现将函数临时挂起的功能
  2. 不通点:yeild from后面可接可迭代对象,也可接future对象/协程对象; await 后面必须要接future对象/协程对象
  • asyncio.Task 对象与threading.Thread对象的比较
  1. asyncio.Task 对象差不多与 threading.Thread 对象等效,都是执行任务的一个载体
  2. Task 对象用于驱动协程, Thread 对象用于调用可调用的对象。
  3. Task 对象不由自己动手实例化,而是通过把协程传给 asyncio.ensure_future(…) 函数或loop.create_task(…) 方法获取,而Thread相反。
  4. 获取的 Task 对象已经排定了运行时间;Thread 实例则必须调用 start 方法开启运行,所以可以调控执行任务的顺序。
  5. 如果想终止任务,可以使用 Task.cancel() 实例方法,在协程内部抛出CancelledError 异常。
     
  • 协程与线程安全性对比
  1. 多线程:多线程任务调度过程中,任何时候都可以中断每个线程;因此必须记住保留锁,去保护程序中的重要部分,防止多步操作在执行的过程中中断,防止数据处于无效状态,保证每个线程的的可靠稳定执行。

  2. 协程:默认会做好全方位保护,以防止中断,程序执行由寄存器上下文控制。对协程来说,无需保留锁,在多个线程之间同步操作,协程自身就会同步,因为在任意时刻只有一个协程运行。想交出控制权时,可以使用 await 或 yield from 把控制权交还调度程序。这就是能够安全地取消协程的原因,按照定义,协程只能在挂起的状态处取消;因此可以用Task.cancel()处理 CancelledError 异常,执行异常清理操作。

  • 避免IO阻塞型调用策略
  1. 策略:

    a. 在单独的线程中运行各个阻塞型操作 
    b. 把每个阻塞型操作转换成非阻塞的异步调用使用

  2. 原因:

    a. 采用多线程,每个线程单独调用一个阻塞任务可以实现,但是各个操作系统线程(Python 使用的是这种线程)消耗的内存达兆字节(具体的量取决于操作系统种类)。如果要处理几千个连接,而每个连接都使用一个线程的话,操作系统一般负荷不起。

    b. 把生成器当作协程使用是异步编程的另一种方式。对事件循环来说,调用回调与在暂停的协程上调用 .send() 方法效果差不多。各个暂停的协程是要消耗内存,但是比线程消耗的内存数量级小。

以下两篇教程也相当不错,值得借鉴

详细教程一

详细教程二

猜你喜欢

转载自blog.csdn.net/u012089823/article/details/88408687