本节继续学习 Python 并发编程的另一种实现方式,也就是 Asyncio 并发编程。
我们知道,使用多线程和普通的单线程相比,其运行效率会有极大的提高。但不得不说,多线程虽然有诸多优势,也存在一定的局限性:
为了解决这些问题,Asyncio 并发编程应运而生。
在详细介绍 Asyncio 之前,要先搞清楚什么是同步,什么是异步。所谓同步,是指操作一个接一个地执行,下一个操作必须等上一个操作执行完成之后才能开始执行;而异步是指不同操作间可以相互交替执行,如果其中地某个操作被堵塞,程序并不会等待,而是会找出可执行的操作继续执行。
为了更好地区分同步和异步,这里举个例子,假设公司要我们做一份报表,并以邮件的方式提交,则分别以同步和异步的方式完成的过程如下:
了解了同步和异步(以及它们之间的区别)之后,接下来正式开始介绍 Asyncio。
事实上,Asyncio 和其他 Python 程序一样,是单线程的,它只有一个主线程,但可以进行多个不同的任务。这里的任务,指的就是特殊的 future 对象,我们可以把它类比成多线程版本里的多个线程。
这些不同的任务,被一个叫做事件循环(Event Loop)的对象所控制。所谓事件循环,是指主线程每次将执行序列中的任务清空后,就去事件队列中检查是否有等待执行的任务,如果有则每次取出一个推到执行序列中执行,这个过程是循环往复的。
为了简化讲解这个问题,可以假设任务只有两个状态:,分别是预备状态和等待状态:
在这种情况下,事件循环会维护两个任务列表,分别对应这两种状态,并且选取预备状态的一个任务(具体选取哪个任务,和其等待的时间长短、占用的资源等等相关)使其运行,一直到这个任务把控制权交还给事件循环为止。
当任务把控制权交还给事件循环对象时,它会根据其是否完成把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成:如果完成,则将其放到预备状态的列表;反之,则继续放在等待状态的列表。而原先在预备状态列表的任务位置仍旧不变,因为它们还未运行。
这样,当所有任务被重新放置在合适的列表后,新一轮的循环又开始了,事件循环对象继续从预备状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务完成。
值得一提的是,对于 Asyncio 来说,它的任务在运行时不会被外部的一些因素打断,因此 Asyncio 内的操作不会出现竞争资源(多个线程同时使用同一资源)的情况,也就不需要担心线程安全的问题了。
讲完了 Asyncio 的原理,下面结合具体的代码来看一下它的用法。还是以下载网站内容为例,用 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.ensure_future(download_one(site)) for site in sites]
await asyncio.gather(*tasks)
def main():
sites = [
'http://www.cdsy.xyz',
'http://www.cdsy.xyz/computer/programme/C_language/',
'http://www.cdsy.xyz/computer/programme/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()
运行结果为:
注意,此程序运行前,需确保已安装好 aiohttp 模块,此模块可直接执行 pip install aiohttp 命令安装。
上面程序中,Async 和 await 关键字是 Asyncio 的最新写法,表示这个语句(函数)是非阻塞的,正好对应前面所讲的事件循环的概念,即如果任务执行的过程需要等待,则将其放入等待状态的列表中,然后继续执行预备状态列表里的任务。
另外在主函数中,第 22-26 行代码表示拿到事件循环对象,并运行 download_all() 函数,直到其结束,最后关闭这个事件循环对象。
值得一提的,如果读者使用 Python 3.7 及以上版本,则 22-26 行代码可以直接用 asyncio.run(download_all(sites)) 来代替。
至于 Asyncio 版本的函数 download_all(),和之前多线程版本有很大的区别:
注意,Python 3.7+ 版本之后,可以使用 asyncio.create_task(coro) 等效替代 asyncio.ensure_future(coro)。
可以看到,其输出结果显示用时只有 0.12s,比之前的多线程版本效率更高,充分体现其优势。
当然,除了例子中用到的这几个函数,Asyncio 还提供了很多其他的用法,你可以查看 Python 事件循环官方文档进行了解。
通过以上的学习,明显看到了 Asyncio 的强大。但是,任何一种方案都不是完美的,都存在一定的局限性,Asyncio 同样如此。
实际工作中,想用好 Asyncio,特别是发挥其强大的功能,很多情况下必须得有相应的 Python 库支持。前面章节在学习多线程编程中使用的是 requests 库,但本节使用的是 aiohttp 库,原因在于 requests 库并不兼容 Asyncio,而 aiohttp 库兼容。Asyncio 软件库的兼容性问题,在 Python3 的早期一直是个大问题,但是随着技术的发展,这个问题正逐步得到解决。
另外,使用 Asyncio 时,因为在任务调度方面有了更大的自主权,写代码时就得更加注意,不然很容易出错。