目录

一、为什么需要异步编程?

1.1 同步 vs 异步

1.2 阻塞 vs 非阻塞

1.3 多线程、多进程与协程的对比

二、协程是什么?—— 可以“暂停”的函数

2.1 生成器与协程的渊源

2.2 async/await 语法糖

三、asyncio 库实战 —— 让代码飞起来

3.1 事件循环(Event Loop)—— 管家

3.2 定义异步函数

3.3 运行异步任务(Task)

3.4 同时运行多个任务:gather & wait

3.5 超时与取消

四、实战案例:异步爬取网页

4.1 同步版本(requests库)

4.2 异步版本(aiohttp库)

4.3 性能对比小结

五、常见坑点与注意事项

5.1 不能在异步函数中使用 time.sleep

5.2 注意线程安全问题

5.3 异步库的生态

5.4 记住:协程函数必须被驱动

六、总结


一、为什么需要异步编程?

1.1 同步 vs 异步

想象一下,你中午点外卖:

同步模式:你下单后,就一直盯着手机看骑手到哪了,啥事也不干,直到外卖送到。这期间你浪费了大量时间在干等。
异步模式:你下单后,该打游戏打游戏,该写代码写代码。等外卖到了,门铃一响(通知),你再去取餐。

在程序世界里:

同步:一个任务没做完,CPU就一直等它,什么也不干。这是Python默认的执行方式。
异步:一个任务遇到耗时操作(比如读文件、请求网络),就主动让出CPU,让CPU去执行其他任务。等耗时操作完成了,再回来继续执行。

核心差异:异步不是并行,而是“遇到等待,主动切换”。它让单线程也能实现高并发。

1.2 阻塞 vs 非阻塞

阻塞:程序卡在那里,什么都不能做。比如time.sleep(5),这5秒内CPU完全被占着空转。
非阻塞:调用后立刻返回,不卡住。比如发起网络请求后,先去干别的,等数据回来了再处理。

异步编程的目标:把所有阻塞操作变成非阻塞,榨干CPU的每一分潜力。

1.3 多线程、多进程与协程的对比

多线程、多进程,它们和协程有什么区别?

方案 优点 缺点 适合场景
多线程 共享内存,切换开销较小 有全局锁(GIL),线程不安全,调试困难 I/O密集型任务
多进程 真正并行,利用多核 资源占用大,进程间通信麻烦 CPU密集型任务
协程 极低的切换开销,无锁,写法像同步代码 需要手动处理异步逻辑,不能利用多核 高并发I/O密集型

一句话总结:如果程序大部分时间在等待(网络请求、数据库查询、文件读写),协程是性价比最高的选择。

二、协程是什么?—— 可以“暂停”的函数

2.1 生成器与协程的渊源

在Python早期,协程是通过生成器(yield)实现的。生成器可以暂停执行,保存状态,下次再恢复。这正是协程需要的能力。

但用生成器写异步代码太反人类了,到处是yield和send。于是Python 3.5正式引入了async/await语法,让异步代码看起来就像普通的同步代码。

2.2 async/await 语法糖

async def:定义一个协程函数。调用它不会立即执行,而是返回一个协程对象。
await:等待一个可等待对象(另一个协程、Future或Task)。执行到这里,程序会挂起当前协程,去执行别的任务,直到等待完成再回来。

python
import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)   # 模拟耗时操作,非阻塞等待
    print("World")

# 协程函数不会自动运行,需要事件循环驱动
asyncio.run(say_hello())

注意:不能在普通函数里使用await,也不能在协程函数里使用time.sleep(那会真正阻塞整个线程),要用await asyncio.sleep。

三、asyncio 库实战 —— 让代码飞起来

3.1 事件循环(Event Loop)—— 管家

事件循环是异步编程的核心引擎。它就像一个永不停止的调度员,负责:

1. 注册任务(协程)
2. 当任务遇到await时,把它挂起,切换到另一个就绪的任务
3. 监测哪些任务等待的事件已完成,唤醒它们
4. 所有任务完成后,关闭循环

我们通常用asyncio.run(main())来创建、运行并关闭事件循环。

3.2 定义异步函数

python
async def fetch_data(url):
    print(f"开始请求 {url}")
    await asyncio.sleep(2)   # 模拟网络延迟
    print(f"完成请求 {url}")
    return f"数据 from {url}"

3.3 运行异步任务(Task)

单个协程:asyncio.run(fetch_data("http://example.com"))

多个协程并发执行,需要把协程包装成Task。

python
async def main():
    # 创建任务(同时开始执行)
    task1 = asyncio.create_task(fetch_data("url1"))
    task2 = asyncio.create_task(fetch_data("url2"))
    
    # 等待所有任务完成
    result1 = await task1
    result2 = await task2
    print(result1, result2)

asyncio.run(main())

# 输出:
开始请求 url1
开始请求 url2
完成请求 url1
完成请求 url2
数据 from url1 数据 from url2

注意两个请求几乎同时开始,总耗时只有2秒,而不是4秒!这就是并发的威力。

3.4 同时运行多个任务:gather & wait

asyncio.gather():并发执行多个任务,返回结果列表。
asyncio.wait():更灵活,可以设置超时、返回条件。

python
async def main():
    tasks = [fetch_data("url1"), fetch_data("url2"), fetch_data("url3")]
    results = await asyncio.gather(*tasks)
    print(results)   # 顺序对应任务顺序

asyncio.run(main())

3.5 超时与取消

用asyncio.wait_for给任务设置超时:

python
async def main():
    try:
        result = await asyncio.wait_for(fetch_data("slow_url"), timeout=1)
    except asyncio.TimeoutError:
        print("请求超时了!")

四、实战案例:异步爬取网页

我们来对比同步和异步爬虫的性能差距。假设要爬取10个网页。

4.1 同步版本(requests库)

python
import requests
import time

urls = ["https://httpbin.org/delay/1"] * 10   # 每个请求延迟1秒

def sync_fetch():
    start = time.time()
    for url in urls:
        response = requests.get(url)
        print(f"完成 {url}, 状态码 {response.status_code}")
    print(f"同步耗时: {time.time() - start:.2f}秒")

sync_fetch()   # 输出: 同步耗时: 约10秒

4.2 异步版本(aiohttp库)

python
import aiohttp
import asyncio
import time

async def fetch_async(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    start = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_async(session, url) for url in urls]
        await asyncio.gather(*tasks)
    print(f"异步耗时: {time.time() - start:.2f}秒")

asyncio.run(main())   # 输出: 异步耗时: 约1秒

性能提升10倍!同步爬10个网页要等10秒,异步只用了1秒多(加上一点额外开销)。这还只是1秒延迟的例子,如果每个请求耗时5秒,差距会更夸张。

4.3 性能对比小结

同步:总时间 = 单个请求时间 × 请求数量
异步:总时间 ≈ 最慢的那个请求时间(并发足够大时)

注意:异步不是万能的。如果任务是CPU密集型的(比如计算圆周率),异步没用,这时用多进程。

五、常见坑点与注意事项

5.1 不能在异步函数中使用 time.sleep

time.sleep会阻塞整个线程,导致事件循环无法切换任务。一定要用await asyncio.sleep。

错误:

python
async def bad():
    time.sleep(1)   # 阻塞了!

正确:

python
async def good():
    await asyncio.sleep(1)

5.2 注意线程安全问题

asyncio是单线程内的并发,通常不需要锁。但如果混用多线程(例如用run_in_executor),要小心共享数据的同步。

5.3 异步库的生态

很多传统库(如requests、openpyxl)不支持异步。需要使用对应的异步版本:

同步库 异步替代
requests aiohttp / httpx
sqlalchemy databases + asyncpg / aiomysql
file open aiofiles
redis aioredis

5.4 记住:协程函数必须被驱动

如果你定义了一个async def函数,但只是调用它而不await或asyncio.run,它会返回一个协程对象,什么都不会发生。

python
async def test():
    print("不会打印")

test()   # 只是得到一个协程对象,没有输出

六、总结

核心要点回顾

1. 异步编程适合I/O密集型任务,能极大提升并发能力。
2. 协程是可暂停的函数,async/await是它的语法糖。
3. 事件循环是异步的引擎,负责调度任务。
4. 用asyncio.create_task、gather、wait来并发执行多个任务。
5. 实战中替换阻塞库为异步版本,性能提升立竿见影。

如果觉得这篇文章对你有帮助,可以点赞、收藏、关注!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐