欢迎光临
我们一直在努力

Python 异步编程入门:从原理到实战

前言:在日常开发中,我们经常会遇到需要处理大量 I/O 操作的场景(比如网络请求、文件读写、数据库查询)。如果使用传统的同步编程,程序会在 I/O 等待期间阻塞,导致 CPU 利用率极低。而异步编程能够让程序在等待 I/O 操作完成的同时,去执行其他任务,从而显著提升程序的并发性能。本文将从基础原理出发,带大家一步步掌握 Python 中的异步编程,包括 asyncio核心 API 的使用、异步函数的编写以及实战案例演练。

一、同步 vs 异步:核心区别是什么?

在讲解异步编程之前,我们先通过一个简单的例子,理解同步和异步在处理任务时的本质差异。

1.1 同步编程:按顺序执行,阻塞等待

同步编程的逻辑非常直观:任务按顺序执行,一个任务完成后,才能开始下一个任务。如果某个任务遇到 I/O 等待(比如等待网络响应),整个程序会停滞在当前任务,直到 I/O 操作完成。

示例代码(同步版文件读取):

python
import time

def read_file(file_name):
    """模拟文件读取(I/O 操作)"""
    print(f"开始读取文件:{file_name}")
    time.sleep(2)  # 模拟 I/O 等待时间
    print(f"完成读取文件:{file_name}")
    return f"{file_name} 的内容"

# 同步执行三个文件读取任务
start_time = time.time()
read_file("file1.txt")
read_file("file2.txt")
read_file("file3.txt")
end_time = time.time()

print(f"同步执行总耗时:{end_time – start_time:.2f} 秒")
    

运行结果:

Plain Text
开始读取文件:file1.txt
完成读取文件:file1.txt
开始读取文件:file2.txt
完成读取文件:file2.txt
开始读取文件:file3.txt
完成读取文件:file3.txt
同步执行总耗时:6.00 秒

可以看到,三个任务依次执行,每个任务的 2 秒等待时间是累加的,总耗时约 6 秒。这就是同步编程的局限性:I/O 等待期间,CPU 完全闲置。

1.2 异步编程:非阻塞执行,任务切换

异步编程的核心是“非阻塞”和“任务切换”。当程序遇到 I/O 等待时,不会一直阻塞,而是会暂停当前任务,去执行其他就绪的任务;当之前的 I/O 操作完成后,再回到暂停的任务继续执行。

还是以文件读取为例,异步版本的执行流程是:

  • 启动读取 file1.txt 的任务,遇到 I/O 等待时暂停;
  • 切换到读取 file2.txt 的任务,遇到 I/O 等待时暂停;
  • 切换到读取 file3.txt 的任务,遇到 I/O 等待时暂停;
  • 当任意一个文件读取完成(I/O 就绪),切换回该任务继续处理;
  • 所有任务完成后,总耗时接近单个任务的 I/O 等待时间(约 2 秒)。
  • 二、Python 异步编程基础:核心概念与 API

    Python 3.4+ 引入了 asyncio 模块,专门用于支持异步编程。在学习具体用法之前,我们需要先掌握几个核心概念:

    2.1 核心概念

    • 协程(Coroutine):异步编程的核心执行单元,类似于函数,但可以在执行过程中暂停并切换到其他协程,等待条件满足后再恢复执行。在 Python 中,通过 async def 定义协程函数。
    • 事件循环(Event Loop):协程的调度器,负责管理所有协程的执行、任务切换和 I/O 事件监听。可以把它理解为一个“任务管理器”,不断循环查看哪些任务可以执行、哪些需要等待。
    • 任务(Task):用于将协程包装成可调度的对象,让事件循环能够管理它。通过 asyncio.create_task() 可以将协程转换为任务。
    • Future:表示一个“未来”才能完成的结果(比如 I/O 操作的结果),是协程之间传递结果的容器。

    2.2 核心 API 速查表

    API 函数

    作用

    async def 函数名()

    定义协程函数

    await 表达式

    暂停协程执行,等待表达式(通常是协程或 Future)完成并返回结果

    asyncio.run(coro)

    启动事件循环,运行指定的协程,执行完成后关闭事件循环(Python 3.7+)

    asyncio.create_task(coro)

    将协程包装成任务,加入事件循环调度(Python 3.7+)

    asyncio.gather(*coros)

    并发运行多个协程,等待所有协程完成后返回结果列表

    asyncio.sleep(delay)

    模拟异步等待(非阻塞等待),delay 秒后恢复协程执行

    三、实战入门:编写第一个异步程序

    下面我们将同步版的文件读取程序改写成异步版本,通过代码实操理解协程、事件循环和任务的使用。

    3.1 异步版文件读取示例

    python
    import asyncio
    import time

    async def async_read_file(file_name):
        """异步模拟文件读取(I/O 操作)"""
        print(f"开始读取文件:{file_name}")
        # 注意:这里必须使用异步的等待方法(asyncio.sleep),不能用 time.sleep(同步阻塞)
        await asyncio.sleep(2)  
        print(f"完成读取文件:{file_name}")
        return f"{file_name} 的内容"

    async def main():
        """主协程:管理所有异步任务"""
        # 方式 1:使用 asyncio.gather 并发运行多个协程
        start_time = time.time()
        result1, result2, result3 = await asyncio.gather(
            async_read_file("file1.txt"),
            async_read_file("file2.txt"),
            async_read_file("file3.txt")
        )
        end_time = time.time()
        
        print(f"\\n异步执行总耗时:{end_time – start_time:.2f} 秒")
        print(f"任务结果:{result1}, {result2}, {result3}")

    # 启动事件循环,运行主协程
    if __name__ == "__main__":
        asyncio.run(main())
       

    3.2 运行结果与分析

    Plain Text
    开始读取文件:file1.txt
    开始读取文件:file2.txt
    开始读取文件:file3.txt
    完成读取文件:file1.txt
    完成读取文件:file2.txt
    完成读取文件:file3.txt

    异步执行总耗时:2.00 秒
    任务结果:file1.txt 的内容, file2.txt 的内容, file3.txt 的内容

    对比同步版本,异步版本的总耗时从 6 秒缩短到 2 秒,性能提升非常明显。关键注意点:

    1. 协程函数必须用 async def 定义,调用协程函数不会直接执行,只会返回一个协程对象;

    2. 必须在协程函数内部使用 await 关键字等待其他协程或异步操作,await 会触发任务切换;

    3. 不能在协程中使用同步的阻塞操作(比如 time.sleep、普通的文件读取 open()),否则会阻塞整个事件循环,导致异步失效。如果需要处理同步 I/O 操作,需要使用 loop.run_in_executor() 将其包装成异步操作。

    四、进阶实战:异步网络请求

    网络请求是 I/O 密集型场景的典型代表,下面我们通过异步网络请求案例,进一步巩固异步编程的用法。这里需要使用 aiohttp 库(异步 HTTP 客户端),先安装依赖:

    Plain Text
    pip install aiohttp

    4.1 异步爬取多个网页内容

    python
    import asyncio
    import aiohttp
    import time

    async def fetch_url(session, url):
        """异步获取网页内容"""
        async with session.get(url) as response:
            # 等待响应并读取内容(异步操作)
            content = await response.text()
            return f"URL: {url}, 状态码: {response.status}, 内容长度: {len(content)}"

    async def main():
        """主协程:并发爬取多个网页"""
        urls = [
            "https://www.baidu.com",
            "https://www.csdn.net",
            "https://www.github.com",
            "https://www.python.org"
        ]
        
        # 创建异步 HTTP 客户端会话
        async with aiohttp.ClientSession() as session:
            # 构建任务列表
            tasks = [fetch_url(session, url) for url in urls]
            
            start_time = time.time()
            # 并发执行所有任务
            results = await asyncio.gather(*tasks)
            end_time = time.time()
            
            # 打印结果
            for result in results:
                print(result)
            print(f"\\n异步爬取总耗时:{end_time – start_time:.2f} 秒")

    if __name__ == "__main__":
        asyncio.run(main())
        

    4.2 运行结果与分析

    运行程序后,会发现 4 个网页的爬取任务几乎同时开始,总耗时远小于逐个爬取的时间(同步爬取总耗时约为 4 个网页的响应时间之和)。关键知识点:

    • aiohttp.ClientSession():创建异步 HTTP 会话,用于发送异步网络请求;
    • async with:异步上下文管理器,用于自动管理资源(比如关闭会话、释放连接),避免资源泄漏;
    • 任务列表推导式:将多个协程包装成任务列表,通过 asyncio.gather(*tasks) 并发执行。

    五、常见问题与避坑指南

    5.1 误区 1:在协程中使用同步阻塞操作

    如果在协程中使用 time.sleep()requests.get()(同步 HTTP 库)等同步阻塞操作,会导致整个事件循环被阻塞,所有协程都无法执行。

    解决方案:

    • 使用异步替代方案:比如用 asyncio.sleep() 替代 time.sleep(),用 aiohttp 替代 requests
    • 包装同步操作:如果没有异步替代方案,可以使用 loop.run_in_executor() 将同步操作放到线程池或进程池中执行,避免阻塞事件循环。

    5.2 误区 2:忘记 await 协程

    如果调用协程函数时没有加await,只会返回一个协程对象,不会实际执行。

    错误示例:

    python
    async def test():
        print("测试协程")

    async def main():
        test()  # 错误:没有 await,协程不会执行

    asyncio.run(main())  # 无任何输出
        

    正确示例:

    python
    async def test():
        print("测试协程")

    async def main():
        await test()  # 正确:await 触发协程执行

    asyncio.run(main())  # 输出:测试协程
        

    5.3 误区 3:事件循环的重复启动

    一个事件循环只能启动一次,多次调用 loop.run_until_complete() 或 asyncio.run() 会报错。

    解决方案:

    • 使用asyncio.run()(Python 3.7+):该函数会自动创建事件循环、运行协程、关闭事件循环,无需手动管理;
    • 手动管理事件循环时,确保只启动一次。

    六、总结与扩展学习

    6.1 核心要点总结

    • 异步编程的核心价值:提升 I/O 密集型任务的并发性能,减少 CPU 闲置时间;
    • Python 异步编程的核心组件:协程(async def)、事件循环(asyncio.run())、任务(asyncio.create_task());
    • 关键关键字:async 定义协程,await 触发任务切换和等待;
    • 避坑核心:协程中避免使用同步阻塞操作,必须 await 协程才能执行。

    6.2 扩展学习方向

    • 深入理解事件循环的工作原理(select/poll/epoll 模型);
    • 学习 asyncio 高级特性:任务取消、超时控制、信号处理;
    • 结合异步框架开发:比如 FastAPI(异步 Web 框架)、Tornado;
    • 了解异步数据库操作:比如 asyncpg(PostgreSQL 异步驱动)、motor(MongoDB 异步驱动)。

    如果本文对你有帮助,欢迎点赞、收藏、关注!如果有任何疑问或补充,欢迎在评论区留言讨论~

    赞(0)
    未经允许不得转载:171主机测评 » Python 异步编程入门:从原理到实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址