深入解析asyncio与协程

本文深入探讨了Python的asyncio库,包括asyncio的基本概念如事件循环、协程、任务和future,以及如何使用async/await定义协程。详细介绍了asyncio的使用,如并发执行、任务管理、协程嵌套、取消运行中协程以及loop的相关方法。还涵盖了asyncio的进阶用法,如利用redis队列动态添加协程,以及适合的使用场景,如异步爬虫和高并发的socket服务。文章最后讨论了aioredis和aiohttp这两个asyncio库的应用,展示了它们在并发操作中的优势。
摘要由CSDN通过智能技术生成

前言

  在前面的文章,已经通过gevent实现高并发的协程,本文将详细讨论Python标准库异步IO——asyncio。在Python3.4中引入了协程的概念以及asyncio。asyncio底层调用yield from语法,将任务变成生成器后挂起,这种方式无法实现协程之间的自动切换,在Python3.5中正式确立引入了async和await 的语法,所有的这些工作都使得Python实现异步编程变得更容易上手。

1、asyncio的基本概念
  • event_loop 事件循环:每一个需要异步执行的任务都要注册到事件循环中,事件循环负责管理和调度这些任务之间的执行流程(遇到IO则自动切换协程等)。

  • coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。

  • task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。

  • future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别

  • async/await 关键字:在python3.5及以上,用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。

  在异步的模式里,所有代码逻辑都会运行在一个forever事件循环中(你可以把整个事件循环看成一个总控中心,它监听着当前线程创建的多个协程发发生的事件),它可以同时执行多个协程,这些协程异步地执行,直到遇到 await 关键字,事件循环将会挂起该协程,事件循环这个总控再把当前线程控制权分配给其他协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。

2、使用asyncio
2.1 使用async关键字和await定义协程

在Python3.5之前,要实现协程方式的写法一般如下:

import asyncio
@asyncio.coroutine
async def mytask(task_id):
    yield from asyncio.sleep(1) 

在Python3.5以后,全面使用async关键字和await定义协程,代码显更直观。

import asyncio
import datetime

def get_time():
    d=datetime.datetime.now()
    return d.strftime('%H:%M:%S')

# async 定义了mytask为协程对象
async def mytask(task_id):
    # 这里就像gevent的sleep方法模拟IO,而且该协程会被asyncio自动切换
    print('task-{} started at:{}'.format(task_id,get_time()))
    await asyncio.sleep(1) # await 要求该行语句的IO是有返回值的例如response=request.get(url),如果直接使用await time.sleep(2),则无法创建协程对象
    print('task-{} done at:{}'.format(task_id,get_time()))

# 创建事件循环对象,该事件循环由当前主线程拥有 
loop = asyncio.get_event_loop()
tasks=[mytask(i) for i in range(4)] # 这里mytask()是协程对象,不会离开运行。
loop.run_until_complete(asyncio.wait(tasks)) # 这里实行的逻辑就像gevent.joinall(tasks)一样,表示loop一直运行直到所有的协程tasks都完成

  代码中通过async关键字定义一个协程(coroutine),不过该协程不能直接运行,需将它注册到事件循环loop里面,由后者在协程内部发生IO时(asyncio.sleep(2))时候调用协程。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。
输出,从结果可以看出,5个协程同一时刻并发运行。

task-1 started at:17:12:37
task-0 started at:17:12:37
task-3 started at:17:12:37
task-2 started at:17:12:37
task-4 started at:17:12:37
task-1 done at:17:12:38
task-0 done at:17:12:38
task-3 done at:17:12:38
task-2 done at:17:12:38
task-4 done at:17:12:38

关于await要求该句为返回值:
await asyncio.sleep(2),这里可以看看sleep返回什么

@coroutine
def sleep(delay, result=None, *, loop=None):
    """Coroutine that completes after a given time (in seconds)."""
    if delay == 0:
        yield
        return result

    if loop is None:
        loop = events.get_event_loop()
    future = loop.create_future()
    h = future._loop.call_later(delay,
                                futures._set_result_unless_cancelled,
                                future, result)
    try:
        return (yield from future)
    finally:
        h.cancel()

如果设为delay值,且loop事件循环已创建(即使代码未创建它也会自动创建),返回的是future对象(yield from future),而这里可以挂起当前协程,直到future完成

2.2 task对象
...同上
# async 定义了mytask为协程对象
async def mytask(task_id):
    # 这里就像gevent的sleep方法模拟IO,而且该协程会被asyncio自动切换
    print('task-{} started at:{}'.format(task_id,get_time()))
    await asyncio.sleep(1)
    print('task-{} done at:{}'.format(task_id,get_time()))
    return 'ok'

coro=mytask(1)
loop = asyncio.get_event_loop()
task=loop.create_task(coro) # 将协程对象封装为task对象
print('before register to loop:',task)
loop.run_until_complete(future=task)
print('after loop completed,task return the result:',task.result())

查看打印结果:

before register to loop: <Task pending coro=<mytask() running at /opt/asyn.py:9>>
task-1 started at:17:39:06
task-1 done at:17:39:07
after loop completed,task return the result: ok

将协程封装为task对象后,task在注册到事件循环之前为pending状态,1秒后,task 结束,并且通过task.result()可以获取协程结果值。
task对象也可用asyncio.ensure_future(coro)创建(接收coro协程或者future对象),它内部封装了loop.create_task

2.2 future对象

前面定义说了future表示将来执行或没有执行的任务的结果,task是future的子类。
基本的方法有:
• cancel(): 取消future的执行,调度回调函数
• result(): 返回future代表的结果
• exception(): 返回future中的Exception
• add_done_callback(fn): 添加一个回调函数,当future执行的时候会调用这个回调函数。
• set_result(result): 将future标为运行完成,并且设置return值,该方法常用
使用future,可以在协程结束后自行回调函数:

import asyncio
...同上
async def coru_1(future_obj,N):
    print('coru_1 started at:{}'.format(get_time()))
    total=sum(range(N))
    await asyncio.sleep(2)
    future_obj.set_result('coru_1 returns:{}'.format(total))
    print('coru_1 done at:{}'.format(get_time()))

async def coru_2(future_obj,N):
    print('coru_2 started at:{}'.format(get_time()))
    total=sum(range(N))
    await asyncio.sleep(2)
    future_obj.set_result('coru_2 returns:{}'.format(total))
    print('coru_2 done at:{}'.format(get_time()))

def call_back(future_obj):
    time.sleep(1)
    print('saved to redis at :',get_time(),future_obj,future_obj.result())

if __name__=='__main__':
    loop=asyncio.get_event_loop()
    f1=asyncio.Future()
    f2=asyncio.Future()
    tasks=[coru_1(f1,10),coru_2(f2,20)]
    f1.add_done_callback(call_back)
    f2.add_done_callback(call_back)
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

输出

coru_1 started at:16:52:07
coru_2 started at:16:52:07
coru_1 done at:16:52:09
coru_2 done at:16:52:09
saved to redis at : 16:52:10 <Future finished result='coru_1 returns:45'> coru_1 returns:45
saved to redis at : 16:52:11 <Future finished result='coru_2 returns:190'> coru_2 returns:190

两个协程同时启动且在同一时间结束运行。之后开始回调,可以看到协程1先回调,1秒完成后,再切换到协程2回调。

2.3 获取协程并发执行后的所有返回值
import asyncio
import datetime

def get_time():
    d=datetime.datetime.now()
    return d.strftime('%H:%M:%S')

async def read_file(task_id):
    print('task-{} started at:{}'.format(task_id,get_time()))
    await asyncio.sleep(2) # 模拟读取文件的耗时IO
    return 'task-{} done at:{}'.format(task_id,get_time())
    
loop = asyncio.get_event_loop()
coros=[read_file(i) for i in range(5)] # 创建多个协程
tasks=[asyncio.ensure_future(coro) for coro in coros]# 将协程封装为task对象 
loop.run_until_complete(asyncio.wait(tasks))
# 或者loop.run_until_complete(asyncio.gether(*tasks))

# 重点在这里,当所有的协程结束后,可批量获取所有协程的返回结果
get_all_result=[ t.result() for t in tasks]
print(get_all_result)

输出:

task-0 started at:15:53:08
task-1 started at:15:53:08
task-2 started at:15:53:08
task-3 started at:15:53:08
['task-0 done at:15:53:09', 'task-1 done at:15:53:09', 'task-2 done at:15:53:09', 'task-3 done at:15:53:09']

以上也无需使用future的回调机制获取协程返回值,直接在loop结束后,从task对象的result方法即可获得协程返回值。
需要注意的是:
用于等待所有协程完成的方法asyncio.wait和asyncio.gather,都是接受多个future或coro组成的列表,区别:asyncio.gather内边调用ensure_future方法将列表中不是task的coro封装为future对象,而wait则没有。

2.4 asyncio.gather vs asyncio.wait

这里再给两个例子说明这两者的区别以及应用场合:
asyncio.gather

import asyncio
import datetime

def get_time():
    d=datetime.datetime.now()
    return d.strftime('%M:%S')

async def coro(group_id,coro_id):
    print('group{}-task{} started at:{}'.format(group_id,coro_id,get_time()))
    await asyncio
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值