Dev

Python asyncio 核心概念与必用技巧

Python asyncio 核心概念与必用技巧

引言

Python 的 asyncio 是编写并发代码的利器,尤其适合 I/O 密集型任务。很多开发者觉得异步编程难以理解,其实只要掌握几个核心概念,就能写出高效的异步程序。本文总结我在实际开发中每天都会用到的 asyncio 知识。

一、核心概念(必须理解)

1. 事件循环 (Event Loop)

事件循环是 asyncio 的心脏,它负责调度和执行异步任务。

import asyncio

# 获取当前事件循环
loop = asyncio.get_running_loop()  # Python 3.7+ 推荐

# 运行一个协程直到完成
async def main():
    print("Hello")

asyncio.run(main())  # 自动创建、运行、关闭事件循环

💡 实际开发中,你几乎不需要手动操作事件循环asyncio.run() 搞定一切。

2. 协程 (Coroutine)

async def 定义的就是协程,它是异步函数。

# 定义协程
async def fetch_data():
    await asyncio.sleep(1)
    return {"data": 42}

# 调用协程会返回协程对象,不会立即执行
coro = fetch_data()
print(type(coro))  # <class 'coroutine'>

# 运行协程的三种方式
# 方式1:asyncio.run()(最常用)
result = asyncio.run(fetch_data())

# 方式2:await(在另一个协程中)
async def main():
    result = await fetch_data()

# 方式3:create_task(并发执行,见下文)

3. await 关键字

await 用于挂起当前协程,让事件循环去执行其他任务。只有可等待对象才能被 await:

  • 协程对象
  • Future 对象
  • Task 对象
async def demo():
    # ✅ 正确:await 协程
    result = await fetch_data()
    
    # ❌ 错误:await 普通函数
    # await time.sleep(1)  # TypeError
    
    # ✅ 正确:await asyncio 提供的可等待对象
    await asyncio.sleep(1)

4. Task 任务

Task 是并发执行的关键。它将协程包装成任务,提交给事件循环调度。

async def task_func(name, delay):
    await asyncio.sleep(delay)
    print(f"Task {name} done")
    return name

async def main():
    # 创建任务(立即开始调度,但还未执行)
    task1 = asyncio.create_task(task_func("A", 2))
    task2 = asyncio.create_task(task_func("B", 1))
    
    # 等待任务完成,同时获取返回值
    result1 = await task1
    result2 = await task2
    print(f"Results: {result1}, {result2}")
    # 输出顺序:Task B done, Task A done
    # 总耗时约2秒,而非3秒

asyncio.run(main())

⚠️ 重要:创建任务后若没有 await,任务可能永远不会执行。

5. Future 对象

Future 是低层级的可等待对象,代表一个尚未完成的操作。Task 是 Future 的子类。日常开发中几乎不会直接使用 Future,但理解它有助于理解 asyncio 原理。

# 很少直接使用,了解即可
future = asyncio.Future()
async def set_result():
    await asyncio.sleep(1)
    future.set_result("Done")

asyncio.create_task(set_result())
result = await future  # 等待结果

二、开发必用技巧

1. 并发执行多个协程

import asyncio

async def fetch(url):
    await asyncio.sleep(1)
    return f"Data from {url}"

# 方式1:gather - 最常用,保持顺序
async def use_gather():
    results = await asyncio.gather(
        fetch("url1"),
        fetch("url2"),
        fetch("url3"),
    )
    print(results)  # 顺序与传入一致

# 方式2:wait - 更灵活的并发控制
async def use_wait():
    tasks = [fetch(f"url{i}") for i in range(3)]
    done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
    for task in done:
        print(task.result())

# 方式3:as_completed - 谁先完成先处理谁
async def use_as_completed():
    tasks = [fetch(f"url{i}") for i in range(3)]
    for coro in asyncio.as_completed(tasks):
        result = await coro
        print(result)  # 顺序不固定

2. 超时控制

async def slow_operation():
    await asyncio.sleep(10)
    return "Done"

async def with_timeout():
    try:
        # 等待最多5秒
        result = await asyncio.wait_for(slow_operation(), timeout=5)
    except asyncio.TimeoutError:
        print("操作超时了!")
        # 可选:取消任务
        # task.cancel()

3. 优雅地取消任务

async def cancellable_task():
    try:
        while True:
            await asyncio.sleep(1)
            print("Working...")
    except asyncio.CancelledError:
        print("任务被取消,执行清理工作")
        # 清理资源
        raise  # 重新抛出,让上层处理

async def main():
    task = asyncio.create_task(cancellable_task())
    await asyncio.sleep(3)
    task.cancel()  # 发送取消信号
    
    try:
        await task
    except asyncio.CancelledError:
        print("任务已取消")

4. 同步原语(解决竞态条件)

# Lock - 互斥锁
async def use_lock():
    lock = asyncio.Lock()
    async with lock:
        # 临界区,同一时间只有一个协程能进入
        await asyncio.sleep(1)

# Queue - 生产者-消费者模式(必用!)
async def producer_consumer():
    queue = asyncio.Queue(maxsize=10)
    
    async def producer():
        for i in range(5):
            await queue.put(f"item {i}")
            await asyncio.sleep(0.1)
        await queue.put(None)  # 终止信号
    
    async def consumer():
        while True:
            item = await queue.get()
            if item is None:
                break
            print(f"Processing {item}")
            queue.task_done()
    
    async with asyncio.TaskGroup() as tg:  # Python 3.11+
        tg.create_task(producer())
        tg.create_task(consumer())

5. 使用 TaskGroup(Python 3.11+ 推荐)

TaskGroup 自动管理任务生命周期,出现异常时自动取消所有子任务。

async def main():
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(fetch("url1"))
        task2 = tg.create_task(fetch("url2"))
        task3 = tg.create_task(fetch("url3"))
    # 退出上下文时,自动等待所有任务完成
    results = [t.result() for t in [task1, task2, task3]]

三、常见陷阱与最佳实践

❌ 错误示例

# 1. 忘记 await(协程不会执行)
async def bad():
    fetch_data()  # 忘记了 await!
    
# 2. 在异步代码中使用同步阻塞调用
async def bad():
    time.sleep(1)  # 应使用 await asyncio.sleep(1)

# 3. 创建任务但不 await 或 store
async def bad():
    asyncio.create_task(fetch_data())  # 任务可能被垃圾回收
    await asyncio.sleep(0.1)  # 依赖运气

✅ 正确实践

# 1. 始终 await 或 create_task
async def good():
    result = await fetch_data()  # 方式1:等待结果
    task = asyncio.create_task(fetch_data())  # 方式2:保存任务引用
    # 稍后 await task

# 2. 使用 asyncio.to_thread 处理阻塞代码
async def good():
    result = await asyncio.to_thread(time.sleep, 1)  # 在线程池中执行

# 3. 使用 TaskGroup 自动管理(Python 3.11+)
async def good():
    async with asyncio.TaskGroup() as tg:
        task = tg.create_task(fetch_data())

四、实战模板:异步 Web 请求

import asyncio
import aiohttp

async def fetch_url(session, url):
    try:
        async with session.get(url, timeout=10) as response:
            return await response.text()
    except Exception as e:
        print(f"Error fetching {url}: {e}")
        return None

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        return await asyncio.gather(*tasks)

async def main():
    urls = [
        "https://api.example.com/1",
        "https://api.example.com/2",
        "https://api.example.com/3",
    ]
    results = await fetch_all(urls)
    for url, data in zip(urls, results):
        print(f"{url}: {len(data) if data else 'Failed'} bytes")

asyncio.run(main())

总结

概念使用频率说明
async def / await100%定义和调用协程
asyncio.run()100%入口函数
asyncio.create_task()90%创建并发任务
asyncio.gather()80%并发收集结果
asyncio.wait_for()60%超时控制
asyncio.Queue()40%生产者-消费者
asyncio.Lock()20%保护共享资源
TaskGroup30%新项目推荐使用

掌握以上概念和技巧,你已经能应对 95% 的 asyncio 开发场景。建议从 gathercreate_task 开始实践,逐步深入。

📌 一句话总结async 定义协程,await 让出控制权,create_task 实现并发,gather 收集结果。