• python asyncio 异步 I/O 协程(Coroutine)与运行 上海


    前言

    Python 在 3.5 版本中引入了关于协程的语法糖 async 和 await, 在 python3.7 版本可以通过 asyncio.run() 运行一个协程。
    所以建议大家学习协程的时候使用 python3.7+ 版本,本文示例代码在 python3.8 上运行的。

    协程 coroutines

    协程(coroutines)通过 async/await 语法进行声明,是编写 asyncio 应用的推荐方式。
    例如,以下代码段(需要 Python 3.7+)

    import asyncio
    import time
    
    
    async def fun():
        print(f'hello start: {time.time()}')
        await asyncio.sleep(3)
        print(f'------hello end : {time.time()} ----')
    
    # 运行
    print(fun())
    

    当我们直接使用fun() 执行的时候,运行结果是一个协程对象coroutine object,并且会出现警告

     RuntimeWarning: coroutine 'fun' was never awaited
      print(fun())
    RuntimeWarning: Enable tracemalloc to get the object allocation traceback
    

    在函数前面加了async,这就是一个协程了,运行的时候需使用asyncio.run()来执行(需要 Python 3.7+)

    import asyncio
    import time
    
    
    async def fun():
        print(f'hello start: {time.time()}')
        await asyncio.sleep(3)
        print(f'------hello end : {time.time()} ----')
    
    # 运行
    asyncio.run(fun())
    

    运行结果

    hello start: 1646009849.5220373
    ------hello end : 1646009852.5258074 ----
    

    协程运行三种机制

    要真正运行一个协程,asyncio 提供了三种主要机制:

    • asyncio.run() 函数用来运行最高层级的入口点 "fun()" 函数 (参见上面的示例。)
    • 等待一个协程。 如:await asyncio.sleep(3)
    • asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。

    通过前面第一个示例,知道了asyncio.run()来运行一个协程,接着看 await 等待的使用

    import asyncio
    import time
    
    
    async def fun_a():
        print(f'hello start: {time.time()}')
        await asyncio.sleep(3)
        print(f'------hello end : {time.time()} ----')
    
    
    async def fun_b():
        print(f"world start: {time.time()}")
        await asyncio.sleep(2)
        print(f'------world end : {time.time()} ----')
    
    
    async def main():
        print('start main:')
        await fun_a()
        await fun_b()
        print('-----------end start----------')
    
    
    asyncio.run(main())
    

    运行结果

    start main:
    hello start: 1646010206.405429
    ------hello end : 1646010209.4092102 ----
    world start: 1646010209.4092102
    ------world end : 1646010211.4115622 ----
    -----------end start----------
    

    运行的入口是main(), 遇到await 会先去执行 fun_a(),执行完成后再去执行fun_b()。

    需注意的是,await 后面不能是普通函数,必须是一个可等待对象(awaitable object),Python 协程属于 可等待 对象,因此可以在其他协程中被等待。
    如果一个对象能够被用在 await表达式中,那么我们称这个对象是可等待对象(awaitable object)。很多asyncio API都被设计成了可等待的。
    主要有三类可等待对象:

    • 协程coroutine
    • 任务Task
    • 未来对象Future。

    在前面这个示例中,fun_a() 和 fun_b()是按顺序执行的,这跟我们之前写的函数执行是一样的,看起来没啥差别,接着看如何并发执行2个协程任务
    asyncio.create_task() 函数用来并发运行作为 asyncio 任务的多个协程

    import asyncio
    import time
    
    
    async def fun_a():
        print(f'hello start: {time.time()}')
        await asyncio.sleep(3)
        print(f'------hello end : {time.time()} ----')
    
    
    async def fun_b():
        print(f"world start: {time.time()}")
        await asyncio.sleep(2)
        print(f'------world end : {time.time()} ----')
    
    
    async def main():
        print('start main:')
        task1 = asyncio.create_task(fun_a())
        task2 = asyncio.create_task(fun_b())
        await task1
        await task2
        print('-----------end start----------')
    
    
    asyncio.run(main())
    

    运行结果

    start main:
    hello start: 1646010554.0892649
    world start: 1646010554.0892649
    ------world end : 1646010556.108237 ----
    ------hello end : 1646010557.08811 ----
    -----------end start----------
    

    从运行的结果可以看到,hello start 和 world start 的开启时间是一样的,也就是2个任务是并发执行的。

    并发任务的误区

    当我们知道协程可以实现并发后,于是小伙伴就想小试一下,去模拟并发下载图片,或者去并发访问网站。
    先看第一个误区:
    把上一个示例中的 await asyncio.sleep(3) 换成 time.sleep(3),假设是完成任务需花费的时间。

    import asyncio
    import time
    
    
    async def fun_a():
        print(f'hello start: {time.time()}')
        time.sleep(3)  # 假设是执行请求花费的时间
        print(f'------hello end : {time.time()} ----')
    
    
    async def fun_b():
        print(f"world start: {time.time()}")
        time.sleep(2)  # 假设是执行请求花费的时间
        print(f'------world end : {time.time()} ----')
    
    
    async def main():
        print('start main:')
        task1 = asyncio.create_task(fun_a())
        task2 = asyncio.create_task(fun_b())
        await task1
        await task2
        print('-----------end start----------')
    
    
    asyncio.run(main())
    

    运行结果

    start main:
    hello start: 1646010901.340716
    ------hello end : 1646010904.3481765 ----
    world start: 1646010904.3481765
    ------world end : 1646010906.3518314 ----
    -----------end start----------
    

    从运行结果看到,并没有实现并发的效果。这是因为time.sleep()它是一个同步阻塞的模块,不是异步库,达不到并发的效果。
    同样道理,之前很多同学学过的 requests 库,知道 requests 库可以发请求,于是套用上面的代码,也是达不到并发效果. 因为 requests 发送请求是串行的,即阻塞的。发送完一条请求才能发送另一条请求。

    如果想实现并发请求,需用到发送 http 请求的异步库,如:aiohttp,grequests等。

  • 相关阅读:
    并不对劲的loj3124:uoj477:p5405:[CTS2019]氪金手游
    并不对劲的loj6498. 「雅礼集训 2018 Day2」农民
    并不对劲的loj2251:p3688[ZJOI2017]树状数组
    并不对劲的loj2050:p3248:[HNOI2016]树
    并不对劲的BJOI2020
    并不对劲的loj3110:p5358:[SDOI2019]快速查询
    并不对劲的loj3111:p5359:[SDOI2019]染色
    (困难) CF 484E Sign on Fence,整体二分+线段树
    算法录 之 拓扑排序和欧拉路径。
    数据结构录 之 BST的高级应用。
  • 原文地址:https://www.cnblogs.com/yoyoketang/p/15944227.html
Copyright © 2020-2023  润新知