什么是异步编程?

01-24 4704阅读

Python asyncio异步编程:从入门到实战指南

在现代软件开发中,处理大量并发任务已成为常态。无论是构建高性能 Web 服务、爬取海量网页,还是实时处理传感器数据,传统同步编程模型往往难以满足性能需求。Python 的 asyncio 模块为此提供了一套优雅的异步编程解决方案,它通过协程(coroutine)和事件循环(event loop)机制,让开发者能以接近同步代码的写法,实现高效的并发执行。本文将带你从基础概念出发,逐步掌握 asyncio 的核心用法,并通过实战案例深入理解其应用场景。

什么是异步编程?

异步编程是一种编程范式,允许程序在等待某些操作(如 I/O、网络请求)完成时,不阻塞主线程,而是继续执行其他任务。与多线程或多进程不同,异步编程通常在单线程内通过“协作式多任务”实现并发,资源开销更小,上下文切换成本更低。

在 Python 中,asyncio 是官方提供的异步 I/O 框架,自 3.4 版本引入,3.5 起支持 async/await 语法,极大简化了异步代码的编写。

什么是异步编程?

基础概念:协程与事件循环

协程是异步编程的核心。你可以把它理解为一种可以“暂停”和“恢复”的函数。在 Python 中,使用 async def 定义协程函数:

import asyncio

async def say_hello():
    print("Hello")
    await asyncio.sleep(1)  # 模拟异步操作
    print("World")

# 运行协程
asyncio.run(say_hello())

注意:直接调用 say_hello() 不会执行函数体,而是返回一个协程对象。必须通过 asyncio.run() 或在已有事件循环中调度才能真正运行。

事件循环(Event Loop)是 asyncio 的调度中心,负责管理所有协程的执行、暂停与恢复。asyncio.run() 会自动创建并启动一个事件循环,运行传入的协程,完成后关闭循环。

并发执行多个任务

单个协程只能顺序执行,真正的并发需要同时调度多个协程。asyncio 提供了多种方式实现并发:

使用 asyncio.create_task()

import asyncio

async def fetch_data(task_id, delay):
    print(f"Task {task_id} started")
    await asyncio.sleep(delay)
    print(f"Task {task_id} finished")
    return f"Data from task {task_id}"

async def main():
    # 创建多个任务
    task1 = asyncio.create_task(fetch_data(1, 2))
    task2 = asyncio.create_task(fetch_data(2, 1))

    # 等待所有任务完成
    result1 = await task1
    result2 = await task2

    print(result1, result2)

asyncio.run(main())

输出顺序表明两个任务是并发执行的:先打印“Task 1 started”,再“Task 2 started”,约1秒后“Task 2 finished”,再1秒后“Task 1 finished”。

使用 asyncio.gather()

当需要同时启动多个协程并收集结果时,gather() 更简洁:

import asyncio

async def main():
    results = await asyncio.gather(
        fetch_data(1, 2),
        fetch_data(2, 1),
        fetch_data(3, 1.5)
    )
    print("All results:", results)

asyncio.run(main())

gather() 会并行执行所有传入的协程,并按参数顺序返回结果列表。

实战:异步 HTTP 请求

假设我们需要从多个 URL 获取数据。使用同步方式逐个请求效率低下,而异步方式可显著提升速度。

首先安装 aiohttp(标准库无异步 HTTP 客户端):

pip install aiohttp

然后编写异步爬虫:

import asyncio
import aiohttp

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

async def main():
    urls = [
        'https://httpbin.org/delay/1',
        'https://httpbin.org/delay/2',
        'https://httpbin.org/delay/1'
    ]

    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        responses = await asyncio.gather(*tasks)
        print(f"Fetched {len(responses)} pages.")

# 注意:实际运行需确保网络环境允许访问
# asyncio.run(main())

此代码在单个会话中并发发起多个请求,总耗时接近最慢请求的时间(约2秒),而非各请求时间之和(约4秒)。

异步上下文管理器与生成器

asyncio 支持异步上下文管理器(async with)和异步生成器(async for),用于处理需要异步初始化/清理的资源或流式数据。

例如,异步文件读取(需第三方库如 aiofiles):

# 示例:伪代码展示异步文件操作结构
# import aiofiles
#
# async def read_file():
#     async with aiofiles.open('data.txt', 'r') as f:
#         content = await f.read()
#         return content

异步生成器可用于逐行处理大文件或实时数据流:

async def async_range(n):
    for i in range(n):
        await asyncio.sleep(0.1)  # 模拟异步延迟
        yield i

async def main():
    async for value in async_range(3):
        print(value)

asyncio.run(main())

常见陷阱与最佳实践

  1. 不要在协程中使用阻塞函数
    time.sleep()requests.get() 等会阻塞整个事件循环。应使用 asyncio.sleep() 或异步替代库(如 aiohttp)。

  2. 避免“假异步”
    即使使用 async/await,若内部调用的是同步 I/O,仍无法获得并发优势。

  3. 合理控制并发数量
    同时发起成千上万个请求可能导致服务器拒绝或本地资源耗尽。可使用 asyncio.Semaphore 限制并发数:

import asyncio

semaphore = asyncio.Semaphore(5)  # 最多5个并发

async def limited_fetch(url):
    async with semaphore:
        # 执行请求
        await asyncio.sleep(0.1)
        return f"Done {url}"

async def main():
    urls = [f"url_{i}" for i in range(20)]
    tasks = [limited_fetch(url) for url in urls]
    await asyncio.gather(*tasks)

asyncio.run(main())

总结与建议

Python 的 asyncio 为 I/O 密集型任务提供了高效、简洁的并发解决方案。通过 async/await 语法,开发者能以清晰的逻辑编写高性能异步程序。然而,异步编程并非万能——它适用于高并发 I/O 场景(如 Web 服务、爬虫、数据库访问),但对 CPU 密集型任务帮助有限,此时应考虑多进程或优化算法。

建议初学者从简单示例入手,理解事件循环与协程的协作机制;在项目中逐步引入异步模式,优先替换明显的 I/O 瓶颈;同时注意工具链选择(如使用 aiohttp 而非 requests)。掌握 asyncio 不仅能提升程序性能,更是迈向现代 Python 开发的重要一步。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。