Python 协程(Coroutine)指南:从入门到实战

📚 前言

在现代 Python 开发中,异步编程已经成为处理高并发场景的标配技术。本文将带你从零开始掌握 Python 协程,通过丰富的代码示例和实战场景,让你彻底理解 async/await 的魔法。


一、为什么需要协程?

1.1 传统并发方案的局限

在 Python 中,我们有三种主要的并发方案:

方案 适用场景 优点 缺点
多线程 I/O 密集型任务 共享内存,切换开销小 GIL 限制,无法利用多核 CPU
多进程 CPU 密集型任务 真正并行,绕过 GIL 内存开销大,进程间通信复杂
协程 高并发 I/O 密集型 极低开销,单线程高并发 不适合 CPU 密集型任务

1.2 协程的核心优势

python 复制代码
# 传统同步代码:串行执行,总耗时 = 各任务耗时之和
import time

def download_file(name):
    print(f"开始下载 {name}")
    time.sleep(2)  # 模拟网络 I/O
    print(f"{name} 下载完成")

start = time.time()
download_file("文件A")
download_file("文件B")
download_file("文件C")
print(f"总耗时: {time.time() - start:.2f}秒")  # 输出约 6 秒

问题:三个文件串行下载,总耗时 6 秒,但实际上网络 I/O 期间 CPU 是空闲的!

📌 协程的解决方案:在等待 I/O 时主动让出 CPU,让其他任务执行,实现"伪并行"。


二、协程核心概念

2.1 关键术语解析

1️⃣ 协程函数(Coroutine Function)

使用 async def 定义的函数:

python 复制代码
async def my_coroutine():
    return "Hello Coroutine"
2️⃣ 协程对象(Coroutine Object)

调用协程函数返回的对象(不会立即执行):

python 复制代码
coro = my_coroutine()  # 此时函数体还未执行
print(type(coro))      # <class 'coroutine'>
3️⃣ 事件循环(Event Loop)

协程的调度器,负责执行协程并管理 I/O 事件:

python 复制代码
import asyncio

asyncio.run(my_coroutine())  # 创建事件循环并运行协程
4️⃣ 可等待对象(Awaitable)

可以被 await 的对象,包括:

  • 协程对象
  • Task 对象(asyncio.create_task() 创建)
  • Future 对象

三、渐进式代码示例

3.1 基础示例:第一个协程

python 复制代码
import asyncio

async def say_hello(name):
    """一个简单的协程函数"""
    print(f"Hello, {name}!")
    await asyncio.sleep(1)  # 模拟异步操作(必须用 asyncio.sleep)
    print(f"Goodbye, {name}!")

# 运行协程的三种方式
# 方式1:推荐(Python 3.7+)
asyncio.run(say_hello("Alice"))

# 方式2:手动管理事件循环(旧版本)
# loop = asyncio.get_event_loop()
# loop.run_until_complete(say_hello("Bob"))

# 方式3:在已有事件循环中运行(Jupyter Notebook)
# await say_hello("Charlie")

运行结果

复制代码
Hello, Alice!
(等待 1 秒)
Goodbye, Alice!

⚠️ 注意await 只能在 async def 函数内部使用,否则会报 SyntaxError


3.2 并发示例:同时执行多个协程

方法一:使用 asyncio.gather()
python 复制代码
import asyncio
import time

async def fetch_data(db_id):
    """模拟从数据库获取数据"""
    print(f"[{time.strftime('%H:%M:%S')}] 开始查询数据库 {db_id}")
    await asyncio.sleep(2)  # 模拟查询耗时
    print(f"[{time.strftime('%H:%M:%S')}] 数据库 {db_id} 查询完成")
    return f"数据_{db_id}"

async def main():
    start = time.time()
    
    # 并发执行三个协程
    results = await asyncio.gather(
        fetch_data(1),
        fetch_data(2),
        fetch_data(3)
    )
    
    print(f"获取结果: {results}")
    print(f"总耗时: {time.time() - start:.2f}秒")

asyncio.run(main())

运行结果

复制代码
[14:23:10] 开始查询数据库 1
[14:23:10] 开始查询数据库 2
[14:23:10] 开始查询数据库 3
[14:23:12] 数据库 1 查询完成
[14:23:12] 数据库 2 查询完成
[14:23:12] 数据库 3 查询完成
获取结果: ['数据_1', '数据_2', '数据_3']
总耗时: 2.01秒  # 注意:不是 6 秒!
方法二:使用 asyncio.create_task()
python 复制代码
async def main():
    start = time.time()
    
    # 创建任务(立即开始执行)
    task1 = asyncio.create_task(fetch_data(1))
    task2 = asyncio.create_task(fetch_data(2))
    task3 = asyncio.create_task(fetch_data(3))
    
    # 等待所有任务完成
    result1 = await task1
    result2 = await task2
    result3 = await task3
    
    print(f"结果: {result1}, {result2}, {result3}")
    print(f"总耗时: {time.time() - start:.2f}秒")

asyncio.run(main())

📌 小贴士gather() 适合批量执行,create_task() 适合需要单独控制任务的场景。


3.3 实战示例:异步 HTTP 请求

场景:批量爬取网页
python 复制代码
import asyncio
import aiohttp  # 需要安装:pip install aiohttp
import time

async def fetch_url(session, url):
    """异步获取单个 URL 的内容"""
    try:
        async with session.get(url, timeout=10) as response:
            content = await response.text()
            print(f"✅ {url} - 状态码: {response.status}, 长度: {len(content)}")
            return content
    except Exception as e:
        print(f"❌ {url} - 错误: {e}")
        return None

async def main():
    urls = [
        "https://www.python.org",
        "https://www.github.com",
        "https://www.stackoverflow.com",
        "https://www.reddit.com",
        "https://www.wikipedia.org"
    ]
    
    start = time.time()
    
    # 创建共享的 HTTP 会话
    async with aiohttp.ClientSession() as session:
        # 并发请求所有 URL
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    print(f"\n总耗时: {time.time() - start:.2f}秒")
    print(f"成功获取: {sum(1 for r in results if r)} 个页面")

asyncio.run(main())

运行结果示例

复制代码
✅ https://www.github.com - 状态码: 200, 长度: 245678
✅ https://www.python.org - 状态码: 200, 长度: 123456
✅ https://www.stackoverflow.com - 状态码: 200, 长度: 345678
✅ https://www.reddit.com - 状态码: 200, 长度: 456789
✅ https://www.wikipedia.org - 状态码: 200, 长度: 234567

总耗时: 1.85秒  # 如果用 requests 同步请求,可能需要 5-10 秒
成功获取: 5 个页面

3.4 进阶示例:异步数据库操作

python 复制代码
import asyncio
import aiosqlite  # 需要安装:pip install aiosqlite

async def create_table(db_path):
    """创建数据表"""
    async with aiosqlite.connect(db_path) as db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                email TEXT
            )
        """)
        await db.commit()
        print("✅ 数据表创建成功")

async def insert_user(db_path, user_id, name, email):
    """插入用户数据"""
    async with aiosqlite.connect(db_path) as db:
        await db.execute(
            "INSERT INTO users (id, name, email) VALUES (?, ?, ?)",
            (user_id, name, email)
        )
        await db.commit()
        print(f"✅ 插入用户: {name}")

async def query_users(db_path):
    """查询所有用户"""
    async with aiosqlite.connect(db_path) as db:
        async with db.execute("SELECT * FROM users") as cursor:
            rows = await cursor.fetchall()
            print("\n📋 用户列表:")
            for row in rows:
                print(f"  ID: {row[0]}, 姓名: {row[1]}, 邮箱: {row[2]}")

async def main():
    db_path = "test.db"
    
    # 创建表
    await create_table(db_path)
    
    # 并发插入多个用户
    await asyncio.gather(
        insert_user(db_path, 1, "Alice", "alice@example.com"),
        insert_user(db_path, 2, "Bob", "bob@example.com"),
        insert_user(db_path, 3, "Charlie", "charlie@example.com")
    )
    
    # 查询用户
    await query_users(db_path)

asyncio.run(main())

运行结果

复制代码
✅ 数据表创建成功
✅ 插入用户: Alice
✅ 插入用户: Bob
✅ 插入用户: Charlie

📋 用户列表:
  ID: 1, 姓名: Alice, 邮箱: alice@example.com
  ID: 2, 姓名: Bob, 邮箱: bob@example.com
  ID: 3, 姓名: Charlie, 邮箱: charlie@example.com

四、常见注意事项

⚠️ 1. 不能在协程中使用同步阻塞库

python 复制代码
# ❌ 错误示例:使用 requests(同步库)
import asyncio
import requests

async def bad_example():
    response = requests.get("https://www.python.org")  # 会阻塞事件循环!
    return response.text

# ✅ 正确示例:使用 aiohttp(异步库)
import aiohttp

async def good_example():
    async with aiohttp.ClientSession() as session:
        async with session.get("https://www.python.org") as response:
            return await response.text()

📌 常用异步库推荐

  • HTTP 请求:aiohttphttpx
  • 数据库:aiosqliteasyncpg(PostgreSQL)、aiomysql
  • Redis:aioredis
  • 文件操作:aiofiles

⚠️ 2. 避免在协程中执行 CPU 密集型任务

python 复制代码
import asyncio

async def cpu_intensive():
    # ❌ 这会阻塞事件循环
    result = sum(i * i for i in range(10_000_000))
    return result

# ✅ 正确做法:使用 run_in_executor 在线程池/进程池中执行
import concurrent.futures

def compute_sum(n):
    """独立函数,用于在进程池中执行"""
    return sum(i * i for i in range(n))

async def cpu_intensive_correct():
    loop = asyncio.get_event_loop()
    # 对于 CPU 密集型任务,使用 ProcessPoolExecutor
    # 注意:在 Windows 上需要 if __name__ == '__main__' 保护
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(
            pool, 
            compute_sum,
            10_000_000
        )
    return result

⚠️ 3. 正确处理异常

python 复制代码
import asyncio

async def risky_task(task_id):
    if task_id == 2:
        raise ValueError(f"任务 {task_id} 出错了!")
    await asyncio.sleep(1)
    return f"任务 {task_id} 完成"

async def main():
    tasks = [risky_task(i) for i in range(1, 4)]
    
    # gather 默认会在第一个异常时停止
    try:
        results = await asyncio.gather(*tasks)
    except ValueError as e:
        print(f"捕获异常: {e}")
    
    # 使用 return_exceptions=True 继续执行其他任务
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for i, result in enumerate(results, 1):
        if isinstance(result, Exception):
            print(f"任务 {i} 失败: {result}")
        else:
            print(f"任务 {i} 成功: {result}")

asyncio.run(main())

⚠️ 4. 协程超时控制

python 复制代码
import asyncio

async def slow_task():
    await asyncio.sleep(5)
    return "完成"

async def main():
    try:
        # 设置 2 秒超时
        result = await asyncio.wait_for(slow_task(), timeout=2.0)
    except asyncio.TimeoutError:
        print("任务超时!")

asyncio.run(main())

五、总结与延伸学习

🎯 核心要点回顾

  1. 协程 ≠ 多线程:协程是单线程内的任务切换,适合 I/O 密集型场景。
  2. 三大核心async def 定义协程,await 等待结果,事件循环调度执行。
  3. 并发工具asyncio.gather() 批量执行,create_task() 单独控制。
  4. 库的选择 :必须使用支持异步的库(如 aiohttp 而非 requests)。

📚 延伸学习建议

  1. 深入理解事件循环

    • 学习 asyncio.get_event_loop() 的底层机制
    • 了解 uvloop(更快的事件循环实现)
  2. 异步上下文管理器

    python 复制代码
    async with aiohttp.ClientSession() as session:
        # 自动管理资源
  3. 异步生成器

    python 复制代码
    async def async_range(n):
        for i in range(n):
            await asyncio.sleep(0.1)
            yield i
    
    async for num in async_range(5):
        print(num)
  4. 实战项目

    • 构建异步爬虫框架
    • 开发高性能 Web API(FastAPI)
    • 实现异步消息队列消费者

六、常见问题速查表

问题 解决方案
RuntimeError: asyncio.run() cannot be called from a running event loop 在 Jupyter 中使用 await 而非 asyncio.run()
协程没有执行 必须用 awaitasyncio.run() 运行协程对象
TypeError: object async_generator can't be used in 'await' expression 异步生成器需要用 async for 而非 await
如何在同步代码中调用协程? 使用 asyncio.run(coro())loop.run_until_complete(coro())
协程中如何使用 time.sleep() 必须用 await asyncio.sleep() 代替
如何取消正在运行的任务? 使用 task.cancel() 并捕获 asyncio.CancelledError
协程能提升 CPU 密集型任务性能吗? 不能,应使用多进程(ProcessPoolExecutor
如何调试协程? 启用调试模式:asyncio.run(main(), debug=True)

📖 参考资料

相关推荐
2401_850491651 小时前
解决Socket图像传输中断问题:基于TCP的可靠图片传输教程
jvm·数据库·python
2301_783848651 小时前
如何在UI中高亮显示近三天更新过的数据行_时间差高亮规则
jvm·数据库·python
努力学习_小白1 小时前
SE注意力机制——学习记录
pytorch·python·深度学习
u0110225121 小时前
JavaScript中Tree-shaking失效的场景及其优化对策
jvm·数据库·python
IT策士1 小时前
Python 面试系列:常见 100 个经典面试问题,从入门到进阶
开发语言·python·面试
阿正呀1 小时前
如何显著提升 Google Sheets 数据库批量更新脚本的执行效率
jvm·数据库·python
dFObBIMmai1 小时前
MySQL迁移过程如何避免数据不一致_利用强一致性备份方案
jvm·数据库·python
驼同学.1 小时前
【求职季】LeetCode Hot 100 渐进式扫盲手册(Python版)
python·算法·leetcode
li星野1 小时前
二分查找六题通关:从标准模板到旋转数组(Python + C++)
java·c++·python