Python-Asyncio学习笔记(一)

相关知识点

Future
future是一个数据结构,表示还未完成的工作结果。事件循环可以监视Future对象是否完成。从而允许应用的一部分等待另一部分完成一些工作。Future
获取Futrue里的结果
future表示还没有完成的工作结果。事件循环可以通过监视一个future对象的状态来指示它已经完成。future对象有几个状态:

Pending/Running/Done/Cancelled


创建future的时候,task为pending,事件循环调用执行的时候当然就是running,调用完毕自然就是done,如果需要停止事件循环,就需要先把task取消,状态为cancel。
Task
task是Future的一个子类,它知道如何包装和管理一个协程的执行。任务所需的资源
可用时,事件循环会调度任务允许,并生成一个结果,从而可以由其他协程消费。

event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。

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

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

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

什么是 Asyncio

事实上,Asyncio 和其他 Python 程序一样,是单线程的,它只有一个主线程,但可以进行多个不同的任务。这里的任务,指的就是特殊的 future 对象,我们可以把它类比成多线程版本里的多个线程。

这些不同的任务,被一个叫做事件循环(Event Loop)的对象所控制。所谓事件循环,是指主线程每次将执行序列中的任务清空后,就去事件队列中检查是否有等待执行的任务,如果有则每次取出一个推到执行序列中执行,这个过程是循环往复的。

为了简化讲解这个问题,可以假设任务只有两个状态:,分别是预备状态和等待状态:

  • 预备状态是指任务目前空闲,但随时待命准备运行;
  • 等待状态是指任务已经运行,但正在等待外部的操作完成,比如 I/O 操作。

在这种情况下,事件循环会维护两个任务列表,分别对应这两种状态,并且选取预备状态的一个任务(具体选取哪个任务,和其等待的时间长短、占用的资源等等相关)使其运行,一直到这个任务把控制权交还给事件循环为止。

当任务把控制权交还给事件循环对象时,它会根据其是否完成把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成:如果完成,则将其放到预备状态的列表;反之,则继续放在等待状态的列表。而原先在预备状态列表的任务位置仍旧不变,因为它们还未运行。

这样,当所有任务被重新放置在合适的列表后,新一轮的循环又开始了,事件循环对象继续从预备状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务完成。

值得一提的是,对于 Asyncio 来说,它的任务在运行时不会被外部的一些因素打断,因此 Asyncio 内的操作不会出现竞争资源(多个线程同时使用同一资源)的情况,也就不需要担心线程安全的问题了

获取事件循环

首先,event loop 就是一个普通 Python 对象,您可以通过 asyncio.new_event_loop() 创建无数个 event loop 对象。只不过,loop.run_xxx() 家族的函数都是阻塞的,比如 run_until_complete() 会等到给定的 coroutine 完成再结束,而 run_forever() 则会永远阻塞当前线程,直到有人停止了该 event loop 为止。所以在同一个线程里,两个 event loop 无法同时 run,但这不能阻止您用两个线程分别跑两个 event loop。

初始情况下,get_event_loop() 只会在主线程帮您创建新的 event loop,并且在主线程中多次调用始终返回该 event loop;而在其他线程中调用 get_event_loop() 则会报错,除非您在这些线程里面手动调用过 set_event_loop()。

new_event_loop()是创建一个eventloop对象,而set_event_loop(eventloop对象)是将eventloop对象指定为当前线程的eventloop,一个线程内只允许运行一个eventloop,,意味着不能有两个eventloop交替运行。这两者一般搭配使用,用于给非主线程创建eventloop。如果是主线程,则只需要get_event_loop就可以了,也就是说,我们想运用携程,首先要生成一个loop对象,然后loop.run_xxx()就可以运行携程了,而如何创建这个loop,对于主线程是loop=get_event_loop().对于其他线程需要首先loop=new_event_loop(),然后set_event_loop(loop)

以下低层级函数可被用于获取、设置或创建事件循环:

asyncio.get_running_loop()

返回当前 OS 线程中正在运行的事件循环。

如果没有正在运行的事件循环则会引发 RuntimeError。 此函数只能由协程或回调来调用。

3.7 新版功能.

asyncio.get_event_loop()

获取当前事件循环。

如果当前 OS 线程没有设置当前事件循环,该 OS 线程为主线程,并且 set_event_loop() 还没有被调用,则 asyncio 将创建一个新的事件循环并将其设为当前事件循环。

由于此函数具有相当复杂的行为(特别是在使用了自定义事件循环策略的时候),更推荐在协程和回调中使用 get_running_loop() 函数而非 get_event_loop()

应该考虑使用 asyncio.run() 函数而非使用低层级函数来手动创建和关闭事件循环。

asyncio.set_event_loop(loop)

将 loop 设置为当前 OS 线程的当前事件循环。

asyncio.new_event_loop()

创建一个新的事件循环。

运行和停止循环

loop.run_until_complete(future)

运行直到 future ( Future 的实例 ) 被完成。

返回 Future 的结果 或者引发相关异常。

loop.run_forever()

运行事件循环直到 stop() 被调用。

如果 stop() 在调用 run_forever() 之前被调用,循环将轮询一次 I/O 选择器并设置超时为零,再运行所有已加入计划任务的回调来响应 I/O 事件(以及已加入计划任务的事件),然后退出。

如果 stop() 在 run_forever() 运行期间被调用,循环将运行当前批次的回调然后退出。 请注意在此情况下由回调加入计划任务的新回调将不会运行;它们将会在下次 run_forever() 或 run_until_complete() 被调用时运行。

loop.stop()  在python3.7中已经取消了

停止事件循环。

loop.is_running()

返回 True 如果事件循环当前正在运行。

loop.is_closed()

如果事件循环已经被关闭,返回 True 。

loop.close()

关闭事件循环。

实例

实例1:

# coding=utf-8
import asyncio
import functools
import logging
import time

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [*] %(processName)s  %(threadName)s  %(message)s"
)


# def done_callback (loop, futu):
#     loop.stop()


async def work01 (x):
    logging.info(f'Waiting :{str(x)}')
    await asyncio.sleep(x)
    logging.info(f'Done :{str(x)}')


# async def work02 (
#         loop,  # 第二种运行方式
#         x
# ):
#     logging.info(f'Waiting :{str(x)}')
#     await asyncio.sleep(x)
#     logging.info(f'Done :{str(x)}')
#     loop.stop()  # 第二种运行方式


if __name__ == '__main__':
    start = time.time()
    
    loop = asyncio.get_event_loop()
    # 第一种运行方式
    loop.run_until_complete(work01(1))
    loop.run_until_complete(work01(3))
    # 第二种运行方式( 第二个协程没结束,loop 就停止了——被先结束的那个协程给停掉的。)
    # asyncio.ensure_future(work02(loop, 1))
    # asyncio.ensure_future(work02(loop, 3))
    # 解决第二种运行方式的最佳方法
    # futus = asyncio.gather(work02(loop, 1), work02(loop, 3))
    # futus.add_done_callback(functools.partial(done_callback, loop))
    
    # loop.run_forever()
    loop.close()
    logging.info(f"<程序退出> 总用时:{time.time() - start}")

输出

2021-07-13 10:55:48,985 [*] MainProcess  MainThread  Waiting :1
2021-07-13 10:55:49,993 [*] MainProcess  MainThread  Done :1
2021-07-13 10:55:49,993 [*] MainProcess  MainThread  Waiting :3
2021-07-13 10:55:52,995 [*] MainProcess  MainThread  Done :3
2021-07-13 10:55:52,995 [*] MainProcess  MainThread  <程序退出> 总用时:4.013116359710693

实例2:

import asyncio
import aiohttp
import time
async def download_one(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            print('Read {} from {}'.format(resp.content_length, url))
async def download_all(sites):
    tasks = [asyncio.ensure_future(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)
def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
   
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(download_all(sites))
    finally:
        loop.close()
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
   
if __name__ == '__main__':
    main()

结果

Read 52053 from http://c.biancheng.net
Read 30718 from http://c.biancheng.net/c
Read 34470 from http://c.biancheng.net/python
Download 3 sites in 0.12174049999999999 seconds

注意,此程序运行前,需确保已安装好 aiohttp 模块,此模块可直接执行 pip install aiohttp 命令安装。

上面程序中,Async 和 await 关键字是 Asyncio 的最新写法,表示这个语句(函数)是非阻塞的,正好对应前面所讲的事件循环的概念,即如果任务执行的过程需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表里的任务。

另外在主函数中,第 22-26 行代码表示拿到事件循环对象,并运行 download_all() 函数,直到其结束,最后关闭这个事件循环对象。

值得一提的,如果读者使用 Python 3.7 及以上版本,则 22-26 行代码可以直接用 asyncio.run(download_all(sites)) 来代替。

至于 Asyncio 版本的函数 download_all(),和之前多线程版本有很大的区别:

  1. 这里的 asyncio.ensure_future(coro) 表示对输入的协程 coro 创建一个任务,安排它的执行,并返回此任务对象。可以看到,这里对每一个网站的下载,都创建了一个对应的任务。

    注意,Python 3.7+ 版本之后,可以使用 asyncio.create_task(coro) 等效替代 asyncio.ensure_future(coro)。

  2. asyncio.gather() 表示在事件循环对象中运行 aws 序列的所有任务。


可以看到,其输出结果显示用时只有 0.12s,比之前的多线程版本效率更高,充分体现其优势。

Asyncio有缺陷吗?

通过以上的学习,明显看到了 Asyncio 的强大。但是,任何一种方案都不是完美的,都存在一定的局限性,Asyncio 同样如此。

实际工作中,想用好 Asyncio,特别是发挥其强大的功能,很多情况下必须得有相应的 Python 库支持。前面章节在学习多线程编程中使用的是 requests 库,但本节使用的是 aiohttp 库,原因在于 requests 库并不兼容 Asyncio,而 aiohttp 库兼容。Asyncio 软件库的兼容性问题,在 Python3 的早期一直是个大问题,但是随着技术的发展,这个问题正逐步得到解决。

另外,使用 Asyncio 时,因为在任务调度方面有了更大的自主权,写代码时就得更加注意,不然很容易出错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值