本文解释 Python 的异步模块 asyncio 的概念和基本用法,并且演示如何通过 Python 脚本操作无头浏览器 pyppeteer 。
一、Python 异步编程的由来历史上,Python 并不支持专门的异步编程语法,因为不需要。 有了多线程(threading)和多进程(multiprocessing),就没必要一定支持异步了。如果一个线程(或进程)阻塞,新建其他线程(或进程)就可以了,程序不会卡死。 [size=1.6em] [size=1.6em]但是,多线程有"线程竞争"的问题,处理起来很复杂,还涉及加锁。对于简单的异步任务来说(比如与网页互动),写起来很麻烦。 [size=1.6em] [size=1.6em]Python 3.4 引入了 asyncio 模块,增加了异步编程,跟 JavaScript 的async/await 极为类似,大大方便了异步任务的处理。它受到了开发者的欢迎,成为从 Python 2 升级到 Python 3 的主要理由之一。 二、asyncio 的设计[size=1.6em]asyncio 模块最大特点就是,只存在一个线程,跟 JavaScript 一样。 [size=1.6em]由于只有一个线程,就不可能多个任务同时运行。asyncio 是"多任务合作"模式(cooperative multitasking),允许异步任务交出执行权给其他任务,等到其他任务完成,再收回执行权继续往下执行,这跟 JavaScript 也是一样的。 [size=1.6em]由于代码的执行权在多个任务之间交换,所以看上去好像多个任务同时运行,其实底层只有一个线程,多个任务分享运行时间。 [size=1.6em]表面上,这是一个不合理的设计,明明有多线程多进程的能力,为什么放着多余的 CPU 核心不用,而只用一个线程呢?但是就像前面说的,单线程简化了很多问题,使得代码逻辑变得简单,写法符合直觉。 [size=1.6em] [size=1.6em]asyncio 模块在单线程上启动一个事件循环(event loop),时刻监听新进入循环的事件,加以处理,并不断重复这个过程,直到异步任务结束。事件循环的内部机制,可以参考 JavaScript 的模型,两者是一样的。 [size=1.6em] 三、asyncio API[size=1.6em]下面介绍 asyncio 模块最主要的几个API。注意,必须使用 Python 3.7 或更高版本,早期的语法已经变了。 [size=1.6em]第一步,import 加载 asyncio 模块。 [size=1.6em][Python] 纯文本查看 复制代码 import asyncio [size=1.6em]第二步,函数前面加上 async 关键字,就变成了 async 函数。这种函数最大特点是执行可以暂停,交出执行权。 [size=1.6em][Python] 纯文本查看 复制代码 async def main(): [size=1.6em]第三步,在 async 函数内部的异步任务前面,加上await命令。 [size=1.6em][Python] 纯文本查看 复制代码 await asyncio.sleep(1) [size=1.6em]上面代码中,asyncio.sleep(1) 方法可以生成一个异步任务,休眠1秒钟然后结束。 [size=1.6em]执行引擎遇到await命令,就会在异步任务开始执行之后,暂停当前 async 函数的执行,把执行权交给其他任务。等到异步任务结束,再把执行权交回 async 函数,继续往下执行。 [size=1.6em]第四步,async.run() 方法加载 async 函数,启动事件循环。 [size=1.6em][Python] 纯文本查看 复制代码 asyncio.run(main()) [size=1.6em]上面代码中,asyncio.run() 在事件循环上监听 async 函数main的执行。等到 main 执行完了,事件循环才会终止。 四、async 函数的示例[size=1.6em]下面是 async 函数的例子,新建一个脚本async.py,代码如下。 [size=1.6em][Python] 纯文本查看 复制代码 #!/usr/bin/env python3
# async.py
import asyncio
async def count():
print("One")
await asyncio.sleep(1)
print("Two")
async def main():
await asyncio.gather(count(), count(), count())
asyncio.run(main()) [size=1.6em]上面脚本中,在 async 函数main的里面,asyncio.gather() 方法将多个异步任务(三个 count())包装成一个新的异步任务,必须等到内部的多个异步任务都执行结束,这个新的异步任务才会结束。 [size=1.6em]脚本的运行结果如下。 [size=1.6em][Python] 纯文本查看 复制代码
$ python3 async.py
One
One
One
Two
Two
Two [size=1.6em]上面运行结果的原因是,三个 count() 依次执行,打印完 One,就休眠1秒钟,把执行权交给下一个 count(),所以先连续打印出三个 One。等到1秒钟休眠结束,执行权重新交回第一个 count(),开始执行 await 命令下一行的语句,所以会接着打印出三个Two。脚本总的运行时间是1秒。 [size=1.6em]作为对比,下面是这个例子的同步版本 sync.py。 [size=1.6em][Python] 纯文本查看 复制代码 #!/usr/bin/env python3
# sync.py
import time
def count():
print("One")
time.sleep(1)
print("Two")
def main():
for _ in range(3):
count()
main() [size=1.6em]上面脚本的运行结果如下。 [size=1.6em][Python] 纯文本查看 复制代码
$ python3 sync.py
One
Two
One
Two
One
Two [size=1.6em]上面运行结果的原因是,三个 count() 都是同步执行,必须等到前一个执行完,才能执行后一个。脚本总的运行时间是3秒。 [size=1.6em]
|