Python 被认为是最容易学习的语言之一,但 Python 的异步编程却令人困惑。本文将介绍 Python 异步编程关键概念和示例(模拟网络请求),使其更易于理解。
特别是,应该从中学到以下几点:
让我们开始吧!
比较基础,关注公众号,后续不断更新…
公众号: 滑翔的纸飞机
异步程序即在不阻塞主进程的情况下并行执行其他操作。简单来说:异步是一种确保程序在进行其他耗时工作时不会无谓地花费时间等待。
通过借助一个简单例子说明这个概念。
忙碌的一个下午…,别问我不是上午,上午基本在睡觉:
步骤 1:洗衣服
步骤 2:搞卫生
步骤 3:泡茶
步骤 4:写博客
我想大伙应该不是这样的(按照步骤),即等待衣服洗完 -> 搞卫生 -> 泡茶 -> 再写博客,顺序按步骤这就是所谓的同步状态。
懂得管理时间应该:
吃完中饭后,首先洗上衣服,然后开始搞卫生,不必等待衣服洗完再搞卫生,当然你会说我要刷会抖音,重点都一样,即同一时间可以通过洗衣机洗衣服然后可以继续搞卫生/刷抖音。当然洗衣服并不会因为我搞卫生而缩减洗衣时间。
好的是,现在不用浪费时间等待每个项目的完成(等待衣服洗完),而是随着下午个人计划进展而执行任务。这意味着多项任务会尽快开始,周末你宝贵时间也会得到有效利用。
综上会发现异步优于同步,可以同一时间干多件事情,但前提需要明白什么时候才用。
异步一个重要特点是,当我们转向其他任务时,顺序就不那么重要了。例如,如果我计划下午按序做三件事情:先洗衣服(趁太阳表现优秀),再搞卫生,最后泡茶。在这种情况下,我们可能需要同步依序完成。可以想象的到基本干完这三件事情可以准备晚饭了。
其二,当考虑采用异步任务时,需思考什么时候继续执行任务?例如:走进阳台打开洗衣机,然后走进杂物间拿出扫把,又走进卧室收拾待洗衣服(阳台就在卧室边上)…
思考后会发现:如上在各项任务之间来回切换,这样做真的能给我们带来任何价值吗?如果在没有太多事情需要等待的时候,异步任务之间来回切换,即产生所谓的执行开销。CPU 在各个任务之间来回奔波。
简而言之,async 并非适用于所有用例,也不会神奇地让你现有的同步代码变得更快。同时异步设计也并非简单。
小结: 异步编程最常用的地方应该是IO密集应用。IO密集包括:网络读取、硬盘读取、数据库读取等等。所以asyncio这个库的名字是:Async + IO,可以看出主要的场景就是IO场景。计算密集应用异步没有太多优势,因为异步优势在于高效使用CPU处理缓慢的IO时可以切换任务使CPU执行其他任务,而高性能计算CPU占用率已经很高,并行才是解决方案,而不是异步。
接下来,让我们来看看异步 python 代码到底是什么样的!
async 和 await 是 Python 3 中用于编写异步程序的关键字。具体语法如下:
异步版本:
async def get_stuff_async() -> Dict:
results = await some_long_operation()
return results["key"]
同步版本:
def get_stuff_sync() -> Dict:
results = some_long_operation()
return results["key"]
异步与同步没有太大区别,唯一的文字区别在于 async 和 await 的存在。那么,async 和 await 究竟是做什么的呢?
async 只是声明当前函数是一个异步操作。await 告诉 Python 这个操作可以暂停,直到 some_long_operation() 函数执行完成。
因此,这两个函数调用区别是:
在 get_stuff_sync
中,我们调用 some_long_operation(),等待调用返回结果,然后返回结果子集(results[“key”])。在等待结果期间,不能执行其他操作,因为这是一个阻塞调用。
在 get_stuff_async
中,我们调度 some_long_operation(),然后将控制权交还给主线程。一旦 some_long_operation() 返回结果,get_stuff_async() 会继续执行,并返回结果子集(results[“key”])。在等待结果期间,主进程可以自由执行其他操作,因为这是一个非阻塞调用。
这是一个抽象的示例,但你可能已经看到了异步方法的一些好处和缺陷。
get_stuff_async():提供了一种更高效的资源使用方法;
get_stuff_sync():提供了更确定的顺序及更简单的实现;
不过,实际使用异步函数和方法要比这个例子复杂一些。
在同步程序中,代码常见运行方式如下:
if __name__ == "__main__":
results = get_stuff_sync()
print(results)
我们会把结果打印到控制台。
如果用我们的异步代码来做这件事,得到的信息会截然不同:
if __name__ == "__main__":
results = get_stuff_async()
print(results)
输出:
sys:1: RuntimeWarning: coroutine 'get_stuff_async' was never awaited
这告诉我们,get_stuff_async() 返回的是一个 coroutine 对象,并不是所预期的结果,同时也告诉了我们原因:函数 "get_stuff_async "从未被等待。
所以,我们只需要在函数调用前面加上"await",对吗?不幸的是,事情没那么简单。我们需要在事件循环中使用 await 来调度我们的逻辑,而不是在顶层使用 “await”。
不然程序依旧抛出错误:
results = await get_stuff_async()
^
SyntaxError: 'await' outside function
那什么是事件循环呢?
Python 异步操作的核心是事件循环。称其为 "事件循环 " 会给它增添几分高深,对吗?事实上,事件循环在各种程序中都有使用,它并不特殊或神奇。
以任何Web服务器为例:Web服务等待用户请求,然后在收到请求时将其视为一个事件,并将该事件与响应相匹配(例如,访问本文的 URL 时,后台会说 “新事件:该浏览器请求文章,我们应返回文章.html”)。这就是一个事件循环。
"事件循环 "指的是 Python 内置的事件循环,它可以让我们安排异步任务(它也适用于多线程和子进程)。你只需知道它能将你安排的任务与事件相匹配,这样它就能知道一个进程何时完成。
要使用它,我们要像这样使用标准库的 asyncio 模块:
import asyncio
if __name__ == "__main__":
# asyncio.run类似于顶级`await`
results = asyncio.run(get_stuff_async())
print(results)
在大多数情况下,我们只需从事件循环中获取信息。在一些高级用例中,编写底层库或服务器代码时可能需要直接访问事件循环,但目前这样就足够了。
因此,我们示例是:
import asyncio
async def some_long_operation():
return {"key": "Introduction to Python3 Asynchronous Programming"}
async def get_stuff_async():
results = await some_long_operation()
return results["key"]
if __name__ == "__main__":
results = asyncio.run(get_stuff_async())
print(results)
示例可以正常运行。但本例过于简单不能体现异步优势,仅仅用来说明异步运行。让我们再看一个复杂点的例子。
异步编程典型示例:向网络上的不同位置发送数据,并通过异步实现:等待请求返回时同时执行其他操作,提高程序效率。
为了说明这个例子,自己编写一个耗时的 API 接口:
# API 接口
import time
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/{sleep}")
def slow(sleep: int):
time.sleep(sleep)
return {"time_elapsed": sleep}
if __name__ == "__main__":
uvicorn.run(app) # uvicorn is a server built with uvloop, an asynchronous event loop!
这是一个简单 API 接口,通过接受URL传递过来的休眠时间,模拟耗时操作。
现在,对于客户端代码,我们随机产生10个数字(用于API接口休眠时长),并发起对于API请求,模拟网络操作。
import asyncio
import aiohttp
from datetime import datetime
from random import randrange
async def get_time(session: aiohttp.ClientSession, url: str):
async with session.get(url) as resp: # 异步上下文管理器
result = await resp.json()
print(result)
return result["time_elapsed"]
async def main(base_url: str):
session = aiohttp.ClientSession(base_url)
# 在 0、10 之间随机选择 10 个数字
numbers = [randrange(0, 10) for i in range(10)]
# 等待每个请求的回应
await asyncio.gather(*[
get_time(session, url)
for url in [f"/{i}" for i in numbers]
])
await session.close()
if __name__ == "__main__":
start_time = datetime.now()
asyncio.run(main("http://localhost:8000"))
print(start_time - datetime.now())
运行这个脚本,输出结果:
[1, 1, 1, 2, 4, 5, 6, 7, 8, 9] # 我们请求应用程序API接口休眠时长
{'time_elapsed': 1} # 等待 X 秒后,API 响应返回数据
{'time_elapsed': 1}
{'time_elapsed': 1}
{'time_elapsed': 2}
{'time_elapsed': 4}
{'time_elapsed': 5}
{'time_elapsed': 6}
{'time_elapsed': 7}
{'time_elapsed': 8}
{'time_elapsed': 9}
9 # 程序运行时间
同步版本客户端代码使用相同的请求时间来实现,对比程序执行时间,代码看起来像这样:
import requests
from datetime import datetime
def get_time(url: str):
resp = requests.get(url)
result = resp.json()
print(result)
return result["time_elapsed"]
def main(base_url: str):
numbers = [1, 1, 1, 2, 4, 5, 6, 7, 8, 9]
print(numbers)
for num in numbers:
get_time(base_url + f"/{num}")
if __name__ == "__main__":
start_time = datetime.now()
main("http://localhost:8000")
print((datetime.now() - start_time).seconds)
输出:
[1, 1, 1, 2, 4, 5, 6, 7, 8, 9]
{'time_elapsed': 1}
{'time_elapsed': 1}
{'time_elapsed': 1}
{'time_elapsed': 2}
{'time_elapsed': 4}
{'time_elapsed': 5}
{'time_elapsed': 6}
{'time_elapsed': 7}
{'time_elapsed': 8}
{'time_elapsed': 9}
44
可以看到异步程序执行 9s,同步 44s行。
需要明白:异步执行程序耗时 9s 是因为最长一次API请求需要休眠 9s,因此程序总耗时9s;而同步耗时是10个API请求耗时累加因此是 44s;
尽管异步和同步调用都要求客户端程序发起10次API请求,并且API接口都等待相同的时间(44 秒),但通过使用异步编程,可以发现每个API接口等待时间并没有减少,但程序整体执行效率更高。
希望本文能为你在 Python 中使用异步编程提供一些参考。这篇文章还远远不够全面,因为并行编程范例还有大量的知识需要学习,但这只是一个开始。
关键点:
(1)异步编程并不总是一劳永逸–在许多使用案例中,同步执行比异步编程更简单、更快速,因为并非每个程序都要坐等数据返回。
(2)使用异步代码需要在设计时考虑程序的顺序以及何时需要哪些数据,而同步代码往往想当然地认为数据就在那里,每次调用都会立即返回。
(3)虽然编写异步代码有多种方法,但几乎总是需要 async/await,如果你不确定是否需要其他方法,你就需要 async/await 语法。