当前位置:首页 > AI 热榜 > 正文内容

Python 异步编程入门:从回调地狱到 async/await 优雅实践

admin3小时前AI 热榜2
本文是一篇面向 Python 开发者的异步编程入门教程,从同步编程的痛点出发,循序渐进讲解 Python 异步编程的核心概念,包括回调、生成器到 async/await 的演进过程,并通过多个原创示例演示如何编写高效的异步代码,帮助开发者理解异步编程的优势与实践方法。

在日常开发中,我们经常会遇到处理 I/O 密集型任务的场景,比如爬取网页、读写文件、调用网络 API 等等。传统的同步编程方式会在等待 I/O 的过程中阻塞当前线程,导致大量时间被浪费。而异步编程则可以让程序在等待 I/O 的时候去处理其他任务,充分利用 CPU 资源,提升程序的整体性能。

Python 对异步编程的支持经历了漫长的演进过程,从早期的回调模式,到基于生成器的实现,最终在 Python 3.5 引入了 async/await 语法,让异步代码变得更加简洁易读。本文将从最基础的概念讲起,带你一步步理解 Python 异步编程的核心思想,并通过实际的代码示例掌握如何编写优雅的异步程序。

同步 vs 异步:到底有什么区别?

在开始学习异步编程之前,我们首先要弄清楚同步和异步的区别。我们先来看一个简单的同步编程示例:

 import time  def download_file(url):     print(f"开始下载 {url}")     # 模拟网络 I/O 等待,耗时 2 秒     time.sleep(2)     print(f"下载完成 {url}")     return f"文件内容来自 {url}"  def main():     start_time = time.time()          # 依次下载三个文件     download_file("https://example.com/file1")     download_file("https://example.com/file2")     download_file("https://example.com/file3")          cost = time.time() - start_time     print(f"总共耗时: {cost:.2f} 秒")  if __name__ == "__main__":     main() 

运行这段代码,你会看到输出结果类似这样:

 开始下载 https://example.com/file1 下载完成 https://example.com/file1 开始下载 https://example.com/file2 下载完成 https://example.com/file2 开始下载 https://example.com/file3 下载完成 https://example.com/file3 总共耗时: 6.01 秒 

很明显,同步方式每次只能等待一个下载完成,才能开始下一个,总耗时是三个文件下载时间的总和。如果使用异步方式,我们可以同时发起三个下载请求,然后一起等待结果,总耗时只需要大约 2 秒。这就是异步编程最大的优势——在处理 I/O 密集型任务时,可以大幅缩短整体执行时间。

那么异步编程到底是如何工作的呢?其实核心思想就是非阻塞等待:当一个任务在等待 I/O 完成时,程序会把 CPU 让给其他任务继续执行,等到 I/O 完成后再回来继续处理这个任务。这样一来,CPU 几乎不会空闲,资源利用率得到了极大提升。

从回调地狱说起:早期的异步实现

在介绍 async/await 之前,我们先来看一下早期异步编程常用的回调模式,理解它为什么会让代码变得难以维护。

回调模式的基本思想是:你发起一个异步操作,然后传递一个函数作为参数,当异步操作完成后,系统会调用你传递的这个回调函数处理结果。我们用 Python 来模拟一下:

 import time from threading import Timer  def async_download(url, callback):     print(f"开始下载 {url}")     # 使用定时器模拟异步 I/O     def on_complete():         result = f"文件内容来自 {url}"         print(f"下载完成 {url}")         callback(result)     Timer(2, on_complete).start()  def main():     start_time = time.time()          def after_download1(result1):         print(f"处理结果1: {result1}")         async_download("https://example.com/file2", lambda result2:              print(f"处理结果2: {result2}")             async_download("https://example.com/file3", lambda result3:                  print(f"处理结果3: {result3}")                 cost = time.time() - start_time                 print(f"总共耗时: {cost:.2f} 秒")             )         )          async_download("https://example.com/file1", after_download1)  if __name__ == "__main__":     main()     # 保持主线程不退出     time.sleep(7) 

你发现问题了吗?如果我们需要按顺序执行多个异步操作,回调函数就会一层套一层,代码缩进会越来越深,阅读和维护都非常困难,这就是大家常说的回调地狱

为了解决回调嵌套的问题,Python 社区开始探索新的异步编程方式,这就引出了基于生成器的异步实现,后来又发展出了 async/await 语法。

async/await:优雅的异步编程语法

Python 3.5 正式引入了 async 和 await 关键字,让我们可以写出既高效又简洁的异步代码。我们先来重写上面的下载示例,感受一下 async/await 的魅力:

 import asyncio import time  async def download_file(url):     print(f"开始下载 {url}")     # 异步等待 I/O 完成,不会阻塞事件循环     await asyncio.sleep(2)     print(f"下载完成 {url}")     return f"文件内容来自 {url}"  async def main():     start_time = time.time()          # 方式一:依次等待三个任务     # await download_file("https://example.com/file1")     # await download_file("https://example.com/file2")     # await download_file("https://example.com/file3")          # 方式二:并发执行三个任务     task1 = asyncio.create_task(download_file("https://example.com/file1"))     task2 = asyncio.create_task(download_file("https://example.com/file2"))     task3 = asyncio.create_task(download_file("https://example.com/file3"))          await task1     await task2     await task3          cost = time.time() - start_time     print(f"总共耗时: {cost:.2f} 秒")  if __name__ == "__main__":     asyncio.run(main()) 

运行这段代码,你会看到输出结果:

 开始下载 https://example.com/file1 开始下载 https://example.com/file2 开始下载 https://example.com/file3 下载完成 https://example.com/file1 下载完成 https://example.com/file2 下载完成 https://example.com/file3 总共耗时: 2.01 秒 

看看!代码结构和同步版本几乎一模一样,但是却实现了并发执行,总耗时从 6 秒缩短到了 2 秒,代码也比回调版本清晰太多了。这就是 async/await 的魔力。

理解核心概念:事件循环、协程、任务

要理解 Python 异步编程,我们需要掌握三个核心概念:事件循环、协程和任务。

事件循环(Event Loop)是异步程序的核心调度器,它负责管理所有待执行的任务,当一个任务等待 I/O 完成时,事件循环会把它暂停,然后去执行其他已经准备好的任务,等到 I/O 完成后再恢复执行这个任务。这样不断循环往复,直到所有任务执行完成。

协程(Coroutine)是 async def 定义的函数,调用它会返回一个协程对象,并不会立即执行。协程可以在等待的时候暂停执行,把控制权交还给事件循环,等条件满足后再继续执行。await 关键字就是用来暂停协程,等待某个操作完成后再继续执行。

任务(Task)是对协程的封装,把协程包装成一个可被事件循环调度的任务。使用 asyncio.create_task() 可以创建任务,让协程开始并发执行,而不是等待它完成。

我们再通过一个示例来理解任务的调度过程:

 import asyncio  async def say_after(delay, message):     await asyncio.sleep(delay)     print(message)  async def main():     print("开始")     # 直接调用协程,只会创建协程对象,不会执行     coro = say_after(1, "你好")     print(f"协程对象: {coro}")          # 把协程包装成任务,开始调度执行     task = asyncio.create_task(coro)     print(f"任务对象: {task}")          # 等待任务完成     await task     print(f"任务完成,结果: {task.result()}")  asyncio.run(main()) 

理解了这三个概念,你就掌握了 Python 异步编程的基本框架。接下来我们来看一些更实用的例子。

实战:并发抓取网页示例

异步编程最常见的应用场景就是并发网络请求,我们来实现一个简单的并发网页抓取示例,对比一下同步和异步的性能差异。

 import asyncio import aiohttp import time  async def fetch_page(session, url):     async with session.get(url) as response:         content = await response.text()         return f"{url}: 抓取到 {len(content)} 字节内容"  async def main():     urls = [         "https://www.example.com",         "https://www.python.org",         "https://www.github.com",         "https://www.baidu.com",     ]          start_time = time.time()          # 创建一个异步 HTTP 会话     async with aiohttp.ClientSession() as session:         # 为每个 URL 创建一个抓取任务         tasks = [asyncio.create_task(fetch_page(session, url)) for url in urls]                  # 等待所有任务完成         results = await asyncio.gather(*tasks)                  # 输出结果         for result in results:             print(result)          total_time = time.time() - start_time     print(f"
全部抓取完成,总耗时: {total_time:.2f} 秒")  if __name__ == "__main__":     asyncio.run(main()) 

这个例子使用了 aiohttp 库来进行异步 HTTP 请求,如果换成同步的 requests 库一个个抓取,总耗时会是所有请求的总和,而使用异步并发抓取,总耗时只略大于最慢的那个请求,大大提升了抓取效率。

需要注意的是,并发抓取并不是无限制的,如果你同时创建上千个任务,可能会对服务器造成过大压力,也可能导致网络阻塞。实际应用中,我们通常会使用信号量来控制并发数量:

 async def bounded_fetch(session, url, semaphore):     # 使用信号量限制并发数量     async with semaphore:         return await fetch_page(session, url)  async def main():     urls = [...]  # 大量 URL 列表     semaphore = asyncio.Semaphore(10)  # 最多允许 10 个并发请求          async with aiohttp.ClientSession() as session:         tasks = [bounded_fetch(session, url, semaphore) for url in urls]         results = await asyncio.gather(*tasks) 

异步编程常见坑点与解决方法

虽然 async/await 语法让异步代码变得更加清晰,但是异步编程仍然有一些容易踩坑的地方,我们来总结几个常见的问题:

1. 忘记给 I/O 操作使用异步版本

最常见的错误就是在异步代码中使用了同步的 I/O 操作,比如在协程中调用 time.sleep() 而不是 asyncio.sleep(),或者使用 requests 库而不是 aiohttp。这样会阻塞整个事件循环,导致所有其他任务都无法执行,异步就变成了同步。

解决方法:在异步代码中,所有可能会阻塞的 I/O 操作都必须使用异步版本。

2. 没有使用 create_task 导致没有并发

很多初学者写出这样的代码:

 await download_file("file1") await download_file("file2") 

这种写法虽然用了 async/await,但是并没有实现并发,总耗时还是累加的。如果需要并发执行,一定要把协程包装成任务再 await。

解决方法:需要并发执行时,先用 asyncio.create_task() 创建任务,然后再 await 所有任务。

3. 异常处理

异步代码的异常处理和同步代码类似,使用 try/except 即可:

 async def main():     try:         result = await some_async_task()     except SomeError as e:         print(f"处理错误: {e}") 

当使用 asyncio.gather() 时,如果其中一个任务抛出异常,gather 会默认抛出这个异常,其他未完成的任务仍然会继续执行,但是不会返回结果。如果你希望不管是否出错都返回所有任务的结果,可以设置 return_exceptions=True 参数:

 results = await asyncio.gather(*tasks, return_exceptions=True) 

异步编程适合什么场景?

最后我们来谈谈异步编程的适用场景。异步编程天生适合I/O 密集型任务,比如:

  • 网络爬虫、API 调用
  • 数据库异步读写
  • WebSocket 服务
  • 大量文件 IO 操作

对于CPU 密集型任务,异步编程并没有优势,因为 CPU 密集型任务会一直占着 CPU,无法靠暂停让出资源。这种情况下,使用多进程处理会更加合适。当然,你也可以结合使用:多进程利用多核 CPU,每个进程内部使用异步处理 I/O。

总结

本文从同步编程的痛点讲起,介绍了 Python 异步编程的发展历程,讲解了事件循环、协程、任务等核心概念,通过多个原创示例演示了如何使用 async/await 编写高效的异步代码,还总结了常见坑点与解决方法,最后分析了异步编程的适用场景。

异步编程并不是银弹,但是掌握它可以让你在处理 I/O 密集型任务时多一件强大的武器,写出性能更高、资源利用率更好的程序。希望本文能帮助你入门 Python 异步编程,开启高效编程之旅。

Python,异步编程,asyncio,async/await,并发编程

相关文章

[AI 热榜] 自动驾驶技术图谱:L4 级量产落地进程分析

自动驾驶技术图谱:L4 级量产落地进程分析 摘要:本文深度解析 自动驾驶 技术在 2024 年的最新发展与应用场景... 🔥 正文内容 随着人工智能技术的飞速发展,自动驾驶、Tesla FS...

[AI 热榜] ChatGPT 进阶指南:微调、RAG 与私有化部署

ChatGPT 进阶指南:微调、RAG 与私有化部署 摘要:本文深度解析 ChatGPT 技术在 2024 年的最新发展与应用场景... 🔥 正文内容 随着人工智能技术的飞速发展,ChatG...

[AI 热榜] AI for Science:药物研发与气候建模突破性进展

AI for Science:药物研发与气候建模突破性进展 摘要:本文深度解析 AI for Science 技术在 2024 年的最新发展与应用场景... 🔥 正文内容 随着人工智能技术的...

[AI 观察] 大模型技术观察:从 LLaMA 3 看开源社区的未来

大模型技术观察:从 LLaMA 3 看开源社区的未来 本文声明:本文基于多方公开资料整理分析,仅代表作者个人观点,不构成任何投资或技术建议。 🔥 一、行业背景 近年来,LLaMA 3、开源、Me...

[AI 观察] AIGC 工具链测评:Midjourney vs Stable Diffusion vs DALL-E 3

AIGC 工具链测评:Midjourney vs Stable Diffusion vs DALL-E 3 本文声明:本文基于多方公开资料整理分析,仅代表作者个人观点,不构成任何投资或技术建议。...

[AI 观察] 虚拟人经济:直播电商领域的规模化复制路径

虚拟人经济:直播电商领域的规模化复制路径 本文声明:本文基于多方公开资料整理分析,仅代表作者个人观点,不构成任何投资或技术建议。 🔥 一、行业背景 近年来,数字人、虚拟主播、直播电商 已成为全球...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。