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)

📖 参考资料

相关推荐
笨鸟先飞,勤能补拙3 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能3 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂4 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study4 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境5 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境5 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd1235 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy12393102166 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb006 小时前
一个项目带你入门AI应用开发01
python
0566466 小时前
Python康复训练——常用标准库
开发语言·python·学习