相关知识点
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(),和之前多线程版本有很大的区别:
- 这里的 asyncio.ensure_future(coro) 表示对输入的协程 coro 创建一个任务,安排它的执行,并返回此任务对象。可以看到,这里对每一个网站的下载,都创建了一个对应的任务。
注意,Python 3.7+ 版本之后,可以使用 asyncio.create_task(coro) 等效替代 asyncio.ensure_future(coro)。
- asyncio.gather() 表示在事件循环对象中运行 aws 序列的所有任务。
可以看到,其输出结果显示用时只有 0.12s,比之前的多线程版本效率更高,充分体现其优势。
Asyncio有缺陷吗?
通过以上的学习,明显看到了 Asyncio 的强大。但是,任何一种方案都不是完美的,都存在一定的局限性,Asyncio 同样如此。
实际工作中,想用好 Asyncio,特别是发挥其强大的功能,很多情况下必须得有相应的 Python 库支持。前面章节在学习多线程编程中使用的是 requests 库,但本节使用的是 aiohttp 库,原因在于 requests 库并不兼容 Asyncio,而 aiohttp 库兼容。Asyncio 软件库的兼容性问题,在 Python3 的早期一直是个大问题,但是随着技术的发展,这个问题正逐步得到解决。
另外,使用 Asyncio 时,因为在任务调度方面有了更大的自主权,写代码时就得更加注意,不然很容易出错。