Python 异步编程入门:搞懂 async/await,这一篇就够了

2026-05-03 20:12 Python 异步编程入门:搞懂 async/await,这一篇就够了已关闭评论

Python 异步编程入门:搞懂 async/await,这一篇就够了

做爬虫时发现同步请求要等好几秒,就想用异步来加快速度,结果被回调地狱和事件循环绕晕了。

问题是什么

我用 requests 写了个简单的爬虫,抓 10 个页面,串行执行花了 12 秒。

直觉告诉我应该「同时发请求」。但 Python 的 GIL 让多线程在 CPU 密集型任务上表现一般,多进程又太重。查了一圈发现,对于 I/O 密集型任务(网络请求、文件读写、数据库查询),异步编程才是正经解法。

结果打开 asyncio 文档那一刻——awaitasync defevent loopcoroutinefuturetask……一堆概念扑面而来,代码写法也和正常的 Python 完全不一样。

解决思路

主流的 Python 并发方案有三条路:

| 方案 | 适用场景 | 心智负担 | 性能 |

|------|---------|---------|------|

| 多线程 (threading) | I/O 密集型 | 低,但有锁问题 | 一般(GIL 限制) |

| 多进程 (multiprocessing) | CPU 密集型 | 中,进程间通信麻烦 | 好,但开销大 |

| 异步 I/O (asyncio) | 高并发 I/O | 高(需要转变思维) | 最好,单线程搞定 |

我的场景是网络 I/O 密集型,选 asyncio 最合适。学习曲线是陡,但收益最大——单线程处理上万并发连接,同步代码做不到。

操作步骤

步骤1:理解核心概念——协程不是函数

异步编程的基础单元是协程(coroutine),不是函数。区别就两个关键字:

```python

# 这是一个普通函数
def hello():
    return "Hello"

# 这是一个协程函数
async def hello_async():
    return "Hello"

调用方式完全不同:

```python

# 普通函数直接调用
result = hello()  # 返回 "Hello"

# 协程函数调用不会执行!返回一个 coroutine 对象
result = hello_async()  # 返回 <coroutine object hello_async at 0x...>
# print(result)  # 不会输出任何东西

**注意**: 这是初学者最容易踩的坑——`async def` 定义的函数,调用后不会立即执行,而是返回一个协程对象。得把它放到事件循环里才能跑。

要用 await 来执行协程:

```python

import asyncio

async def say_hello():
    print("Hello...")
    await asyncio.sleep(1)  # 模拟 I/O 操作
    print("...World!")

# await 必须在 async 函数里用
async def main():
    await say_hello()

# 入口调用
asyncio.run(main())

await asyncio.sleep(1) 这行是精髓:碰到 await,当前协程就「挂起」,事件循环去跑别的协程,等 1 秒到了再回来继续。这就是异步的魔法。

📸[截图位置:控制台输出 "Hello... 等待1秒后 ...World!"]

步骤2:跑起第一个事件循环

事件循环是异步编程的调度中心,它负责:

1. 注册协程任务

2. 协程遇到 await 时挂起它

3. I/O 完成后唤醒对应的协程

4. 不断重复,直到所有任务完成

```python

import asyncio

async def task(name, delay):
    print(f"任务 {name} 开始,需要 {delay} 秒")
    await asyncio.sleep(delay)
    print(f"任务 {name} 完成")
    return f"{name} 的结果"

async def main():
    # 顺序执行(和同步没区别,别这样写)
    result1 = await task("A", 2)
    result2 = await task("B", 1)
    print(result1, result2)

asyncio.run(main())  # 总耗时 ~3秒

上面这是错的——await task("A", 2) 会等 2 秒,然后 await task("B", 1) 再等 1 秒,总共 3 秒,浪费了异步的能力。

要并发就得用 asyncio.gathercreate_task

```python

async def main():
    # 并发执行!正确用法
    task_a = asyncio.create_task(task("A", 2))
    task_b = asyncio.create_task(task("B", 1))
    
    # 注意:虽然 B 后启动,但 B 先完成(只需要 1 秒)
    result_a = await task_a
    result_b = await task_b
    print(result_a, result_b)

asyncio.run(main())  # 总耗时 ~2秒(取最长任务)

看到了吗?create_task 把协程包装成 Task,提交到事件循环后就立即开始执行,后面的 await 只是等结果。两个任务同时在跑了。

📸[截图位置:输出对比——顺序执行 vs 并发执行的时间差异]

步骤3:用 `asyncio.gather` 批量跑任务

create_task 适合任务数量固定的场景。如果有一堆任务要跑,用 gather 更优雅:

```python

import asyncio

async def fetch_url(url):
    print(f"开始请求: {url}")
    await asyncio.sleep(1)  # 模拟网络请求
    return f"数据来自 {url}"

async def main():
    urls = [
        "https://api.example.com/users",
        "https://api.example.com/posts",
        "https://api.example.com/comments",
        "https://api.example.com/tags",
        "https://api.example.com/categories",
    ]
    
    # 并发请求所有 URL
    results = await asyncio.gather(
        *(fetch_url(url) for url in urls)
    )
    
    for result in results:
        print(f"收到: {result}")

asyncio.run(main())  # 总耗时 ~1秒,不是 5 秒!

**注意**: `gather` 返回的结果顺序和传入的任务顺序一致,和任务完成先后无关。上面虽然 5 个请求同时发出,但 `results` 列表的顺序就是 urls 的顺序。

📸[截图位置:5个请求同时发出,1秒后全部完成]

步骤4:实战——用 aiohttp 写真正的异步爬虫

前面用 asyncio.sleep 模拟,现在上真家伙。aiohttp 是 Python 最流行的异步 HTTP 库:

```bash

pip install aiohttp

```python

import asyncio
import aiohttp
import time

async def fetch_one(session, url):
    async with session.get(url) as response:
        # 注意:response.text() 也是异步的
        html = await response.text()
        return len(html)  # 返回页面大小

async def main():
    urls = [
        "https://httpbin.org/delay/1",  # 会等 1 秒返回
        "https://httpbin.org/delay/2",  # 会等 2 秒返回
        "https://httpbin.org/delay/3",  # 会等 3 秒返回
    ]
    
    start = time.time()
    
    # 异步方式:并发请求
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_one(session, url) for url in urls]
        sizes = await asyncio.gather(*tasks)
    
    end = time.time()
    print(f"页面大小: {sizes}")
    print(f"异步耗时: {end - start:.2f} 秒")  # ~3秒(最长那个)

    # 对比:同步方式
    import requests
    start = time.time()
    for url in urls:
        resp = requests.get(url)
        print(f"同步: {len(resp.text)}")
    end = time.time()
    print(f"同步耗时: {end - start:.2f} 秒")  # ~6秒(1+2+3)

asyncio.run(main())

输出示例:

```

页面大小: [314, 542, 768]
异步耗时: 3.02 秒
同步: 314
同步: 542
同步: 768
同步耗时: 6.15 秒

异步比同步快了一倍!URL 越多,差距越明显。100 个请求每个耗时 1 秒,同步要 100 秒,异步只需要 1 秒出头。

📸[截图位置:异步 vs 同步的耗时对比输出]

步骤5:踩坑记录——常见的 3 个坑

坑 1:在同步代码里调异步

```python

import asyncio

async def fetch_data():
    await asyncio.sleep(1)
    return "data"

# ❌ 这行会报错
result = fetch_data()
# RuntimeWarning: coroutine 'fetch_data' was never awaited

# ✅ 正确做法
result = asyncio.run(fetch_data())

坑 2:在异步代码里用同步阻塞库

```python

import asyncio
import requests  # 同步库!

async def fetch():
    # ❌ requests.get 是同步阻塞的,会卡住整个事件循环
    resp = requests.get("https://httpbin.org/get")
    return resp.text

async def main():
    tasks = [fetch() for _ in range(10)]
    # 这并没有并发!因为 requests.get 阻塞时事件循环无法切换
    results = await asyncio.gather(*tasks)

得用异步版本的库:aiohttp 替代 requestsaiomysql 替代 pymysqlasyncpg 替代 psycopg2

坑 3:忘了加超时

```python

import asyncio

# ❌ 没有超时,请求挂了协程就卡死
async def fetch_without_timeout():
    # 如果这个请求一直不返回…
    await some_slow_request()

# ✅ 加超时
async def fetch_with_timeout():
    try:
        async with asyncio.timeout(5):  # Python 3.11+
            await some_slow_request()
    except asyncio.TimeoutError:
        print("请求超时了!")

**注意**: Python 3.11 引入的 `asyncio.timeout()` 是上下文管理器用法。3.10 及以下版本用 `asyncio.wait_for()`。

📸[截图位置:超时触发的 TimeoutError 异常信息]

结果与总结

花了一个周末搞懂 Python 异步编程后,我的爬虫从 12 秒降到了 1.2 秒,10 倍的提升。核心收获:

  • **异步不是并行**(不是同时执行多件事),而是**并发**(在等待 I/O 时去做别的事)——单线程搞定
  • 记住三个核心 API:`asyncio.run()` 启动事件循环、`asyncio.create_task()` 提交协程、`asyncio.gather()` 批量并发
  • 异步生态还不完整,很多库没有异步版本,用之前先查一下
  • I/O 密集型场景下性能提升非常明显,但 CPU 密集计算该用多进程还是得多进程
  • 最大的坑是思维模式切换——不能用写同步代码的脑子去写异步代码。习惯了以后,你会发现 await 其实就是一个「让出控制权」的信号:「我现在要等 I/O,你先去执行别的任务,准备好了叫我。」

    延伸思考

  • **异步上下文管理器**:`async with` 用于异步资源管理(如 aiohttp 的 session),用 `__aenter__` 和 `__aexit__` 实现
  • **异步迭代器**:`async for` 用于逐条获取异步数据,比如从数据库游标逐行读取,用 `__aiter__` 和 `__anext__` 实现
  • **框架支持**:FastAPI、Tornado、Sanic 等 Web 框架都原生支持异步,后续可以聊聊怎么用 FastAPI 写一个高并发 API
  • **uvloop**:把事件循环替换成 C 语言实现的 uvloop,性能还能再翻一倍
  • **CPU 密集型怎么办**:用 `asyncio.to_thread()` (Python 3.9+) 可以把 CPU 密集任务扔到线程池里跑,不阻塞主事件循环

  • 截图清单

    1. 📸 控制台输出 Hello... 等待 1 秒后 ...World! 的演示结果

    2. 📸 顺序执行 vs 并发执行的时间对比输出

    3. 📸 5 个 aiohttp 请求并发发出、1 秒后全部完成的输出

    4. 📸 异步(~3 秒)vs 同步(~6 秒)的耗时对比

    5. 📸 超时场景下 asyncio.TimeoutError 异常信息

    你可能感兴趣的文章

    来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
    转载请注明出处: https://teachcourse.cn/4033.html ,谢谢支持!

    资源分享

    分类:Android 标签:
    Android学习笔记十二:Android基础知识 Android学习笔记十二:Android
    Java语法和C#语法的差异比较 Java语法和C#语法的差异比较
    EditPlus注册码 EditPlus注册码
    浅谈Android DVM 浅谈Android DVM

    评论已关闭!