Python异步编程:asyncio深入理解与实战指南

By | 2026年9月7日

引言

在Python开发中,I/O密集型任务(如网络请求、文件读写)常常成为性能瓶颈。传统的同步代码在等待I/O时会阻塞整个线程,导致资源浪费。虽然多线程可以缓解,但线程切换开销和GIL限制让Python的多线程在CPU密集型任务上力不从心。而异步编程通过事件循环和协程,让单个线程在等待I/O时切换到其他任务,极大提升并发性能。Python的asyncio库自3.4引入,已成为异步编程的标准。本文将从基础到实战,深入剖析asyncio的核心,并分享踩坑经验与最佳实践。

1. 核心概念:事件循环、协程、任务与Future

1.1 事件循环(Event Loop)

事件循环是asyncio的心脏,它负责调度协程、处理I/O事件。可以把它想象成一个不断循环的调度器:它检查是否有就绪的任务,执行它们,并处理它们产生的I/O事件。

在Python 3.7+,推荐使用asyncio.run()来启动事件循环,它会自动创建并关闭循环。


import asyncio

async def main():
    print('Hello')
    await asyncio.sleep(1)
    print('World')

asyncio.run(main())

1.2 协程(Coroutine)

协程是异步函数,使用async def定义。调用协程函数不会立即执行,而是返回一个协程对象。协程需要被事件循环调度才能执行。


async def my_coroutine():
    return 42

coro = my_coroutine()  # 不会执行,返回协程对象
# 需要await或包装成任务才能执行

1.3 任务(Task)

任务是对协程的封装,用于并发调度。创建任务后,协程会尽快被事件循环调度执行。使用asyncio.create_task()创建任务。


async def main():
    task = asyncio.create_task(my_coroutine())
    result = await task
    print(result)

asyncio.run(main())

1.4 Future

Future是更低级的对象,表示一个异步操作的最终结果。任务继承自Future。通常我们不需要直接操作Future,但理解它有助于深入理解asyncio。

2. 实战:编写异步HTTP客户端

让我们用asyncio和aiohttp编写一个并发下载多个网页的示例。

首先安装aiohttp:


pip install aiohttp

然后编写代码:


import asyncio
import aiohttp

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

async def main():
    urls = [
        'https://httpbin.org/get',
        'https://httpbin.org/ip',
        'https://httpbin.org/user-agent'
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(fetch(session, url)) for url in urls]
        results = await asyncio.gather(*tasks)
        for result in results:
            print(result[:100])

asyncio.run(main())

这里我们使用了asyncio.gather()来并发运行多个任务,它会在所有任务完成后返回结果列表。

3. 控制并发:信号量与限制

直接创建大量任务可能导致资源耗尽。我们可以使用asyncio.Semaphore来限制并发数。


import asyncio
import aiohttp

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

async def main():
    urls = [f'https://httpbin.org/get?i={i}' for i in range(100)]
    semaphore = asyncio.Semaphore(10)  # 最多同时10个请求
    async with aiohttp.ClientSession() as session:
        tasks = [asyncio.create_task(fetch(session, url, semaphore)) for url in urls]
        results = await asyncio.gather(*tasks)
    print(f'下载了{len(results)}个页面')

asyncio.run(main())

4. 超时控制与取消任务

异步操作可能永远不返回,所以超时控制至关重要。asyncio.wait_for可以设置超时。


import asyncio

async def slow_operation():
    await asyncio.sleep(10)
    return 'done'

async def main():
    try:
        result = await asyncio.wait_for(slow_operation(), timeout=2)
        print(result)
    except asyncio.TimeoutError:
        print('操作超时')

asyncio.run(main())

任务取消:我们可以调用task.cancel()来取消任务。


async def main():
    task = asyncio.create_task(slow_operation())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print('任务被取消')

5. 常见陷阱与最佳实践

陷阱1:在异步代码中调用同步阻塞函数

如果在协程中调用time.sleep()或requests.get(),会阻塞整个事件循环。

错误示例:


import time
import asyncio

async def bad_coroutine():
    time.sleep(1)  # 阻塞!

正确做法:使用asyncio.sleep()或await loop.runinexecutor()。


async def good_coroutine():
    await asyncio.sleep(1)  # 非阻塞

# 对于必须的同步阻塞函数,使用线程池
import asyncio
import time

async def main():
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, time.sleep, 1)  # 在线程池中执行

陷阱2:忘记await协程

如果忘记await,协程不会执行,只会创建一个协程对象。这常导致任务未运行。


async def main():
    coro = my_coroutine()  # 没有await
    print('协程未执行')

陷阱3:在事件循环外创建任务

在事件循环外调用asyncio.create_task()会报错。确保在协程内部调用。

最佳实践:使用asyncio.run()统一管理循环

对于简单脚本,使用asyncio.run()即可。但对于更复杂的应用,可能需要手动管理循环。


async def main():
    # 主逻辑
    pass

if __name__ == '__main__':
    asyncio.run(main())

最佳实践:优雅关闭资源

使用async with管理资源,确保清理。


async def main():
    async with aiohttp.ClientSession() as session:
        # 使用session
        pass

6. 总结与延伸

本文介绍了asyncio的核心概念,并通过实战展示了如何编写并发HTTP客户端、控制并发数、处理超时和取消。同时,我们指出了常见的陷阱和最佳实践。

要深入学习异步编程,可以进一步探索以下主题:

  • asyncio.Queue 用于生产者-消费者模式
  • 异步上下文管理器和异步迭代器
  • 使用asyncio.createsubprocessshell执行子进程
  • 与多线程/多进程的组合使用

异步编程是现代Python开发的重要技能,掌握它能让你写出更高效、更可扩展的应用程序。希望本文能为你打下坚实的基础。