🏠 首页 › 攻略 › Python asyncio 异步编程实战:从入门到高效并发

Python asyncio 异步编程实战:从入门到高效并发

Python asyncio 是处理高并发 IO 的最佳方案。本文从协程基础讲起,涵盖 async/await 用法、并发控制、异步 I/O 实战,适合 Python 开发者进阶。

你还在用多线程搞并发?

Python 里写高并发程序,很多人第一反应是 threading。

线程确实能用,但有个大坑:Python 有 GIL(全局解释器锁),多线程在 CPU 密集型任务上基本是伪并发,只能跑满一个核。

那 IO 密集型呢?比如同时请求100个 API、爬100个网页——线程也能做,但上下文切换开销大,代码还啰嗦。

asyncio 的出现就是为了解决这个问题。 它在单线程里实现伪并发,用协程替代线程,开销小、代码简洁、性能高。

下面我用实际例子,带你从入门到实战。


一、asyncio 基础:协程是什么?

协程(Coroutine)是 asyncio 的核心。它像函数,但可以用 await 暂停,等 IO 完成后继续执行。

import asyncio

async def fetch_data(url):
    print(f"开始请求 {url}")
    await asyncio.sleep(1)  # 模拟网络请求
    print(f"{url} 完成")
    return {"data": "hello"}

# 运行协程
result = asyncio.run(fetch_data("https://api.example.com"))
print(result)

关键点:

  • async def 定义协程函数
  • await 暂停当前协程,等 IO 完成
  • asyncio.run() 是入口,创建事件循环并运行协程

await 的本质:不是"等待",而是"让出控制权"。当前协程暂停,事件循环去跑其他协程,等 IO 完成后再回来继续。


二、并发请求:比多线程快10倍

这是 asyncio 最经典的使用场景。

串行 vs 并发

先看不用 asyncio 的做法——串行请求:

import time
import asyncio

urls = ["https://api.example.com/a", "https://api.example.com/b", "https://api.example.com/c"]

start = time.time()
for url in urls:
    asyncio.run(fetch_data(url))
print(f"串行耗时: {time.time() - start:.2f}秒")
# 输出: 串行耗时: 3.01秒

3个请求各等1秒,总共3秒。

用 asyncio.gather 并发

async def fetch_all():
    tasks = [fetch_data(url) for url in urls]
    results = await asyncio.gather(*tasks)
    return results

start = time.time()
asyncio.run(fetch_all())
print(f"并发耗时: {time.time() - start:.2f}秒")
# 输出: 并发耗时: 1.01秒

3个请求同时发起,总共只等1秒。快了3倍。

asyncio.gather() 的作用:把多个协程打包在一起并发执行,等全部完成后返回结果。


三、控制并发数:别让服务器被打爆

并发不是越多越好。同时发起1000个请求,服务器可能扛不住,你的 IP 也可能被封。

用 asyncio.Semaphore 控制并发数:

async def fetch_with_limit(sem, url):
    async with sem:  # 获取信号量,并发数限制
        return await fetch_data(url)

async def fetch_limited():
    sem = asyncio.Semaphore(10)  # 最多同时10个请求
    tasks = [fetch_with_limit(sem, url) for url in urls]
    results = await asyncio.gather(*tasks)
    return results

这样即使有100个 URL,也最多同时发10个请求。其他90个排队等信号量。


四、异步 I/O:处理文件和网络

异步读写文件

import aiofiles  # 需要 pip install aiofiles

async def read_large_file(path):
    async with aiofiles.open(path, 'r') as f:
        content = await f.read()
    return content

async def write_result(path, data):
    async with aiofiles.open(path, 'w') as f:
        await f.write(data)

注意:标准库的 open() 是同步的,不能用 await。要装 aiofiles。

异步 HTTP 请求

import aiohttp

async def fetch_with_session(session, url):
    async with session.get(url) as resp:
        return await resp.json()

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

aiohttp 是 asyncio 生态里最成熟的 HTTP 客户端库,比 requests 快得多。


五、实战:写一个异步爬虫

下面是一个完整案例——用 asyncio 并发爬取多个页面标题:

import asyncio
import aiohttp
from bs4 import BeautifulSoup

URLS = [
    "https://news.ycombinator.com",
    "https://github.com/trending",
    "https://stackoverflow.com",
]

async def fetch_page(session, url):
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            html = await resp.text()
            soup = BeautifulSoup(html, 'html.parser')
            title = soup.title.string if soup.title else "No title"
            return url, title
    except Exception as e:
        return url, f"Error: {e}"

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_page(session, url) for url in URLS]
        results = await asyncio.gather(*tasks)
    
    for url, title in results:
        print(f"{url} -> {title}")

asyncio.run(main())

运行结果:

https://news.ycombinator.com -> Hacker News
https://github.com/trending -> Currently trending repositories · GitHub
https://stackoverflow.com -> Stack Overflow - Where Developers Learn & Grow

3个页面并发请求,总共1秒左右搞定。串行可能需要5-6秒。


六、常见坑和解决方案

坑1:忘了 await

async def wrong():
    fetch_data("https://example.com")  # 忘记 await,协程不会执行

解决:所有协程调用前面都要加 await。

坑2:在同步代码里直接调 async 函数

def sync_func():
    result = asyncio.run(fetch_data("https://example.com"))  # 可以,但有坑

如果在已经运行的事件循环里再调 asyncio.run(),会报错:RuntimeError: This event loop is already running。

解决:用 asyncio.create_task() 在已有事件循环里创建任务:

async def main():
    task = asyncio.create_task(fetch_data("https://example.com"))
    result = await task

坑3:混用同步和异步代码

import requests

async def wrong():
    resp = requests.get("https://example.com")  # 同步阻塞,会卡住整个事件循环

解决:异步代码里只用异步库。网络请求用 aiohttp,HTTP 请求用 httpx(支持 async/sync 双模式)。


七、asyncio vs 多线程:怎么选?

场景推荐方案
大量 IO 操作(网络请求、文件读写)asyncio
CPU 密集型计算multiprocessing
简单并发,代码简洁优先threading(配合 GIL 释放的库)
需要异步 + 同步混合asyncio + ThreadPoolExecutor

一句话:IO 密集型选 asyncio,CPU 密集型选 multiprocessing,简单场景选 threading。


八、总结

asyncio 是 Python 高并发编程的利器。掌握以下几点就够用了:

  1. async def + await 是基本语法
  2. asyncio.gather() 并发执行多个协程
  3. asyncio.Semaphore() 控制并发数
  4. 网络请求用 aiohttp,文件操作用 aiofiles
  5. 千万别在异步代码里用同步阻塞调用

你现在的项目里,有没有适合用 asyncio 改造的场景?聊聊看。