多线程已经可以带来较大的效率提升,那么我们还需要asyncio的原因是:
sync和async的概念区分:
Asyncio和python主程序一样,只有一个主线程,但是可以进行多个不同任务(特殊的future对象,可类比为多线程里的多个线程),这些任务被一个event loop的对象所控制。
任务可分为两种状态:预备状态和等待状态
预备状态:任务目前空闲,但随时待命准备运行
等待状态:任务已经运行,但正在等待外部的操作完成,如I/O操作
event loop的执行过程:
对于asyncio来说,它的任务在运行时不会被外部的一些因素打断,因此asyncio内的操作不会出现race condition的情况,就不需要担心线程安全的问题了
以之前博文中下载网站内容的任务为例,其使用Asyncio的写法如下所示(省略异常处理操作):
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.create_task(download_one(site)) for site in sites]
await asyncio.gather(*tasks)
def main():
sites = [
'https://en.wikipedia.org/wiki/Portal:Arts',
'https://en.wikipedia.org/wiki/Portal:History',
'https://en.wikipedia.org/wiki/Portal:Society',
'https://en.wikipedia.org/wiki/Portal:Biography',
'https://en.wikipedia.org/wiki/Portal:Mathematics',
'https://en.wikipedia.org/wiki/Portal:Technology',
'https://en.wikipedia.org/wiki/Portal:Geography',
'https://en.wikipedia.org/wiki/Portal:Science',
'https://en.wikipedia.org/wiki/Computer_science',
'https://en.wikipedia.org/wiki/Python_(programming_language)',
'https://en.wikipedia.org/wiki/Java_(programming_language)',
'https://en.wikipedia.org/wiki/PHP',
'https://en.wikipedia.org/wiki/Node.js',
'https://en.wikipedia.org/wiki/The_C_Programming_Language',
'https://en.wikipedia.org/wiki/Go_(programming_language)'
]
start_time = time.perf_counter()
asyncio.run(download_all(sites))
end_time = time.perf_counter()
print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
if __name__ == '__main__':
main()
## 输出
Read 63153 from https://en.wikipedia.org/wiki/Java_(programming_language)
Read 31461 from https://en.wikipedia.org/wiki/Portal:Society
Read 23965 from https://en.wikipedia.org/wiki/Portal:Biography
Read 36312 from https://en.wikipedia.org/wiki/Portal:History
Read 25203 from https://en.wikipedia.org/wiki/Portal:Arts
Read 15160 from https://en.wikipedia.org/wiki/The_C_Programming_Language
Read 28749 from https://en.wikipedia.org/wiki/Portal:Mathematics
Read 29587 from https://en.wikipedia.org/wiki/Portal:Technology
Read 79318 from https://en.wikipedia.org/wiki/PHP
Read 30298 from https://en.wikipedia.org/wiki/Portal:Geography
Read 73914 from https://en.wikipedia.org/wiki/Python_(programming_language)
Read 62218 from https://en.wikipedia.org/wiki/Go_(programming_language)
Read 22318 from https://en.wikipedia.org/wiki/Portal:Science
Read 36800 from https://en.wikipedia.org/wiki/Node.js
Read 67028 from https://en.wikipedia.org/wiki/Computer_science
Download 15 sites in 0.062144195078872144 seconds
上述代码中用到了Async和await关键字,表示所修饰的语句/函数是non-block的,这对应的便是上面提到的event loop的概念:若任务的执行过程中需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表中的任务。
主函数中的asyncio.run(coro)是Asyncio的root call,表示拿到event loop,运行输入的coro(即协程),直到它结束,最后关闭这个event loop。
Asyncio版本的函数download_all(),和之前多线程版本有很大区别:
tasks = [asyncio.create_task(download_one(site)) for site in sites]
await asyncio.gather(*task)
这里的asyncio.create_task(coro),表示对输入的协程coro创建一个任务,安排它的执行,并返回此任务对象。可以看到,对每个网站,都创建了一个对应的任务。
再往下看,asyncio.gather(*aws, loop=None, return_exception=False),则表示在event loop 中运行*aws序列中的所有任务。
在实际工作中,要想用好Asyncio,必须得有相应的python库支持。在之前的多线程例子中,我们用到的是requests库,而在这里使用的却是aiohttp库,原因就在于requests库与Asyncio不兼容,但aiohttp库兼容。但是兼容问题会随着版本的问题逐步减少。
此外,使用Asyncio使得我们在任务调度方面有更大的自主权,写代码时就得更加注意,否则容易出现错误。
比如,如果你需要await一系列的操作,就得使用asyncio.gather();如果只是单个的future,则用asyncio.wait()就可以了。那么,对于你的future,是想让它run_until_complete()还是run_forever()呢?此类问题都是在面对具体问题时需要去考虑的。
在面对具体问题时,我们可以按照以下伪代码的规范去选择用多线程还是asyncio:
if io_bound:
if io_slow:
print('Use Asyncio')
else:
print('Use multi-threading')
else if cpu_bound:
print('Use multi-processing')
不同于多线程,Asyncio是单线程的,但其内部event loop的机制,使得它可以并发运行多个不同的任务,并且比多线程享有更大的自主控制权。
Asyncio中的任务,在运行过程中不会被打断,因此不会出现race condition的情况。
在I/O heavy的情况下,Asyncio的运行效率比多线程更好。这是因为: