引言

你是否遇到过这样的场景:程序需要同时下载多个网页、处理大量I/O请求,但用普通同步代码时,每个请求都要等待上一个完成,整个程序慢得像老牛拉车?Python的asyncio就是为解决这类问题而生的。它让你能用“单线程+事件循环”的方式,写出高效、易维护的并发程序。

本文将从核心概念出发,配合完整的实战爬虫示例,帮你彻底搞懂asyncio。即使你是异步编程新手,也能跟着代码一步步运行起来,感受异步带来的速度提升。


一、核心概念:协程、事件循环与可等待对象

1. 协程(coroutine)

协程是asyncio的核心。你可以把它理解为一种特殊的函数:它可以在中途“暂停”,把控制权交还给调度器,等条件满足后再从暂停点恢复执行。Python中定义协程非常简单:

import asyncio

async def my_coroutine():
    print("开始执行")
    await asyncio.sleep(1)  # 模拟异步等待
    print("等待结束")

关键点:
- 使用async def定义。
- 内部使用await挂起当前协程,等待另一个可等待对象完成。

2. 事件循环(Event Loop)

事件循环是asyncio的发动机。它负责调度和运行所有协程,处理I/O事件、定时任务等。你可以把它想象成一个不停循环的“管家”,检查哪些协程可以继续执行,并快速切换。

平时使用时,我们一般不需要直接创建事件循环,asyncio.run()会帮我们搞定一切:

asyncio.run(my_coroutine())  # 自动创建事件循环并运行

3. 可等待对象(Awaitable)

能在await后面使用的对象,被称为“可等待对象”。主要分为三类:
- 协程(Coroutines):用async def定义的函数对象。
- 任务(Tasks):用来并发执行多个协程。
- Future:底层对象,代表一个异步操作的最终结果,通常不需要直接创建。

当你在一个协程里await另一个可等待对象时,当前协程会挂起,直到等待的对象完成并返回结果。


二、创建多个任务:并发执行

如果只是顺序await多个协程,那和同步没有区别。要实现真正的并发,需要将协程包装成Task,交给事件循环同时调度。

1. 使用asyncio.create_task()

import asyncio
import time

async def fetch_data(task_id, delay):
    print(f"任务{task_id}开始,等待{delay}秒...")
    await asyncio.sleep(delay)
    print(f"任务{task_id}结束")
    return f"数据{task_id}"

async def main():
    # 创建两个任务,它们会并发运行
    task1 = asyncio.create_task(fetch_data(1, 2))
    task2 = asyncio.create_task(fetch_data(2, 1))

    print("任务已创建,等待完成...")

    # 等待任务完成并获取结果
    result1 = await task1
    result2 = await task2

    print(f"获得结果: {result1}, {result2}")

start = time.time()
asyncio.run(main())
print(f"总耗时: {time.time() - start:.2f}秒")

运行结果:

任务已创建,等待完成...
任务1开始,等待2秒...
任务2开始,等待1秒...
任务2结束
任务1结束
获得结果: 数据1, 数据2
总耗时: 2.00秒

可以看到,两个任务总耗时只有2秒(最长等待时间),而不是3秒(顺序执行的总和)。这正是并发的魅力。

2. 使用asyncio.gather()

gather可以同时运行多个可等待对象,并返回结果列表:

async def main():
    results = await asyncio.gather(
        fetch_data(1, 2),
        fetch_data(2, 1),
        fetch_data(3, 0.5)
    )
    print(f"所有结果: {results}")

gather的方便之处在于,你不需要逐一创建Task,它会自动把协程包装成任务执行。如果其中一个任务抛出异常,默认会立即传播,也可以通过return_exceptions=True将异常当作正常结果返回。


三、实战示例:异步爬虫,速度对比

下面我们写一个完整的异步爬虫,利用aiohttp并发请求多个URL,并与同步版本对比速度。

环境准备:pip install aiohttp

```python
import asyncio
import time
import aiohttp
import requests

要爬取的URL列表(示例用了几个公开的接口)

URLS = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/3",
]

更多推荐