Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手

Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手

你有没有写过这样的代码:给 10 个请求各开一个线程,结果耗时和单线程差不多,CPU 还飙到 100%?或者线程开到几百个,程序直接卡死不动。

这不是你代码写错了,而是用错了并发模型。Python 的多线程在 IO 密集场景下,远没有你想象中那么"多线程"。

这篇文章解决三件事:Python 多线程为什么跑不快、asyncio 凭什么做到高并发、以及我踩过的 5 个异步坑。看完你能直接把爬虫或批量接口调用改写成异步版本。


一、为什么 Python 多线程"跑不快"

1.1 GIL:同一时刻只有一个线程在跑字节码

CPython 有一个全局解释器锁(GIL)。它保证同一时刻只有一个线程在执行 Python 字节码:

  • CPU 密集任务(计算、图像处理、加密):多线程不能利用多核,还会因线程切换增加开销,甚至比单线程更慢;
  • IO 密集任务 (网络请求、文件读写、数据库查询):线程等待 IO 时会释放 GIL,所以多线程有一定作用------但每个线程占用独立的栈内存,开到几百个时调度开销会拖垮程序。
python 复制代码
import threading
import time

def cpu_task():
    s = 0
    for i in range(10_000_000):
        s += i
    return s

start = time.perf_counter()
for _ in range(4):
    cpu_task()
print("单线程:", round(time.perf_counter() - start, 2), "s")

start = time.perf_counter()
ts = [threading.Thread(target=cpu_task) for _ in range(4)]
[t.start() for t in ts]
[t.join() for t in ts]
print("4 线程:", round(time.perf_counter() - start, 2), "s")

在多核机器上跑,你会发现4 线程并没有快 4 倍,通常只是持平甚至更慢。这就是 GIL 的直接结果。

1.2 asyncio 用更低的成本解决"等待"

asyncio 的本质是:单线程 + 事件循环,在等待 IO 的时候不干等,转头去执行别的协程。

它不是并行(parallel),而是并发(concurrency):

维度 线程 协程
内存占用 ~8MB / 个 几 KB / 个
切换成本 操作系统内核态切换 用户态,纯 Python 调度
并发规模 几百个就吃力 几千上万个轻松
CPU 密集 受 GIL 限制 同样受 GIL 限制

关键认知:asyncio 买的是"等待时的效率",不是"计算时的速度"。 这决定了它的适用边界(第五节)。


二、环境准备:Python 3.7+ 自带,无需安装

asyncio 是 Python 标准库 ,3.7 及以上已随解释器安装,不需要 pip install asyncio(装这个包反而会报错)。

bash 复制代码
# 查看 Python 版本,建议 3.7+
python --version

# 实战部分用到异步 HTTP 客户端 aiohttp,单独装它即可
pip install aiohttp

经验提示:3.7 引入 asyncio.run() 让入口极简;3.11 加入更安全的 asyncio.TaskGroup。本文示例以 3.8+ 为准,均可直接运行。


三、四个必须分清的核心概念

  1. 协程(Coroutine) :async def 定义的函数,调用它不会立即执行 ,只返回一个协程对象;必须被 await 或交给事件循环才真正运行。
  2. 事件循环(Event Loop) :异步程序的"调度中枢",不断检查哪个协程可以继续执行。asyncio.run() 帮你隐式创建并管理它。
  3. await :只能在 async 函数内使用,作用是"暂停当前协程,等后面的可等待对象完成",不会阻塞整个线程。
  4. 任务(Task) :把协程包装成可由事件循环调度的对象,asyncio.create_task() 创建,让多个协程并发跑。

一句话记忆:协程是剧本,事件循环是导演,await 是"轮到别人先演",Task 是同时开拍的多条线。

python 复制代码
import asyncio

async def hello():
    print("① 开始执行")
    await asyncio.sleep(1)      # 主动让出控制权,模拟 IO 等待
    print("② 1 秒后继续执行")

coro = hello()          # 注意:这里什么都没打印!只返回协程对象
print(type(coro))       # <class 'coroutine'>
asyncio.run(coro)       # 这一行才真正驱动它执行

await 后面只能跟三类 awaitable 对象:协程、asyncio.Task、asyncio.Future。

python 复制代码
async def main():
    # ❌ 错误:time.sleep 是阻塞的,不是 awaitable
    # await time.sleep(1)
    # ✅ 正确:asyncio.sleep 是可等待的
    await asyncio.sleep(1)

asyncio.run(main())

四、并发实战:让多个任务真正同时跑

create_task 把协程挂到事件循环上立即调度,gather 并发收集结果,总耗时取决于最慢的那个,而不是累加。

python 复制代码
import asyncio

async def job(name, seconds):
    print(f"{name} 开始,需要 {seconds}s")
    await asyncio.sleep(seconds)
    print(f"{name} 完成")
    return name

async def main():
    t1 = asyncio.create_task(job("任务A", 2))
    t2 = asyncio.create_task(job("任务B", 1))
    t3 = asyncio.create_task(job("任务C", 3))
    # 总耗时 ≈ 3s,而非 2+1+3=6s
    results = await asyncio.gather(t1, t2, t3)
    print("全部完成:", results)

asyncio.run(main())

高频 API 速查:

python 复制代码
# 并发执行,按传入顺序返回结果
results = await asyncio.gather(coro1(), coro2(), coro3())

# 立即调度,不阻塞当前协程(发射后不管)
task = asyncio.create_task(coro1())
result = await task

# 超时控制:超过 3 秒抛 TimeoutError
try:
    await asyncio.wait_for(slow_coro(), timeout=3)
except asyncio.TimeoutError:
    print("超时了")

# 限制最大并发数,防止把对方服务器打挂
sem = asyncio.Semaphore(10)

async def limited_fetch(session, url):
    async with sem:
        return await fetch(session, url)

进阶建议:Python 3.11+ 推荐用 async with asyncio.TaskGroup() as tg: 替代裸 gather,某个任务失败时能自动取消其他任务,更安全。

4.1 真实场景:并发抓取多个网站状态码

python 复制代码
import asyncio
import aiohttp

async def fetch_status(session, url):
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            return url, resp.status
    except Exception as e:
        return url, f"错误: {type(e).__name__}"

async def main():
    urls = [
        "https://www.baidu.com",
        "https://www.python.org",
        "https://www.github.com",
    ]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_status(session, u) for u in urls]
        results = await asyncio.gather(*tasks)
    for url, status in results:
        print(f"{url} -> {status}")

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

aiohttp.ClientSession 是异步上下文管理器,必须用 async with 管理,避免连接泄漏。


五、适用边界:什么时候该用,什么时候别用

这是我最想强调的一节------用错场景,asyncio 不仅没收益,还会把代码变复杂。

✅ 该用 asyncio

  • 网络 IO 密集:爬虫、批量调用 HTTP API、WebSocket 长连接;
  • 高并发长连接:聊天服务、实时推送,单线程扛几千连接;
  • 需要精细控制并发度 :配合 Semaphore 做限流。

❌ 别用 asyncio

  • CPU 密集计算 :GIL 仍在,asyncio 帮不上忙,改用 ProcessPoolExecutor 或多进程;
  • 简单脚本:只发 2、3 个请求,同步写法更清晰,别为了"显得高级"上异步;
  • 生态全是同步库 :重度依赖不支持异步的数据库驱动 / SDK 时,强行异步会到处 run_in_executor,得不偿失。

CPU 密集任务的正确做法------把计算丢进进程池,绕开 GIL:

python 复制代码
from concurrent.futures import ProcessPoolExecutor

def cpu_heavy(n):
    return sum(i * i for i in range(n))

async def main():
    loop = asyncio.get_running_loop()
    with ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, cpu_heavy, 10_000_000)
    print(result)

六、我踩过的 5 个坑

坑 1:在协程里用 requests,整条事件循环被卡死

requests 是同步库。在协程里调用会阻塞整个事件循环,并发瞬间退化成串行。

python 复制代码
# ❌ 灾难写法:requests 会阻塞事件循环
async def bad_fetch(url):
    return requests.get(url).status_code

# ✅ 方案A:换异步库 aiohttp
async def good_fetch(session, url):
    async with session.get(url) as r:
        return r.status

# ✅ 方案B:不得不用同步库时,丢进线程池(3.9+ 可用 asyncio.to_thread)
async def legacy_fetch(url):
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(None, lambda: requests.get(url).status_code)

坑 2:忘了 await,协程静默跳过

python 复制代码
async def main():
    fetch(session, url)       # ❌ 没有 await:不报错、不执行、静默跳过
    await fetch(session, url) # ✅

这是最隐蔽的坑。开启 python -W error::RuntimeWarning 可以让"协程从未被等待"直接抛出警告。

坑 3:在 Jupyter 里调 asyncio.run() 报"循环已在运行"

Jupyter 底层(IPython)已经在跑事件循环了。在 cell 里直接 await 即可 ,不要再 run():

python 复制代码
result = await main_async()   # ✅ Jupyter 里直接 await

坑 4:create_task 后没保存引用,任务被垃圾回收

python 复制代码
# ❌ 任务可能在完成前被 GC 回收
asyncio.create_task(some_coro())

# ✅ 保留强引用
_background_tasks = set()
task = asyncio.create_task(some_coro())
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)

坑 5:把 CPU 密集函数放进协程,以为能加速

asyncio 不解决计算问题。把计算函数丢进协程,结果只会和单线程一样慢,还多一层复杂度------用进程池。


七、实战:异步批量抓取 + 限流 + 超时

把上面的知识串成一个能直接用的小工具(50 个请求、并发上限 10):

python 复制代码
import asyncio
import aiohttp
from typing import Iterable

async def fetch_one(session, url, sem, timeout=10):
    async with sem:
        try:
            async with session.get(
                url, timeout=aiohttp.ClientTimeout(total=timeout)
            ) as r:
                return url, r.status
        except Exception as e:
            return url, f"ERROR: {type(e).__name__}"

async def fetch_all(urls: Iterable[str], limit: int = 10):
    sem = asyncio.Semaphore(limit)
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_one(session, u, sem) for u in urls]
        return await asyncio.gather(*tasks)

if __name__ == "__main__":
    urls = ["https://httpbin.org/delay/1" for _ in range(50)]
    results = asyncio.run(fetch_all(urls, limit=10))
    ok = sum(1 for _, s in results if s == 200)
    print(f"完成 {len(results)} 个,成功 {ok} 个")

50 个请求、每个延迟 1 秒、并发上限 10:理论耗时约 5 秒;同步写法需要 50 秒。


八、一页纸总结

  • 多线程跑不快的根因:GIL 限制 CPU 并行 + 线程内存/调度成本高;
  • asyncio 的本质:单线程 + 事件循环,等待 IO 时切换任务,开销极低;
  • 四个概念 :协程(async def)、事件循环(调度中枢)、await(让出)、Task(并发执行);
  • 记住三件事 :async def 定义了不会立即执行、await 才是驱动、asyncio.run() 是入口;
  • 适用边界:IO 密集用它,CPU 密集用进程池,简单脚本别上异步;
  • 最大坑 :协程里调同步库(requests、time.sleep)会阻塞整个循环,要么换异步库,要么丢线程池。

如果你只记住一句:asyncio 买的是"等待时的效率",不是"计算时的速度"。


你第一次用 asyncio 时踩的是哪个坑?是忘了 await,还是被 requests 卡死过整个循环?评论区聊聊,我看到都会回。

如果这篇帮你把异步的概念理顺了,点个赞收藏一下------下一期打算写「asyncio 和多线程到底怎么选」的实测对比,想看的话留言告诉我。

相关推荐
卷福同学1 小时前
第一次当面试官有感
后端·面试
苏三说技术1 小时前
为什么越来越多人用 OnlyOffice?
后端
清桔1 小时前
模型的调用
python
知守观1 小时前
@Transactional 事务失效排查,try-catch 吞异常导致回滚失败(附源码分析)
后端·spring
羑悻1 小时前
Codex + Seed-2.1-pro 实测:多模态理解 + Coding Agent 能扛住真实仓库吗?
后端
颜进强1 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
明月_清风1 小时前
数据进入平台后怎么处理?一文搞懂 ETL
大数据·后端·数据分析
初学AI的小高1 小时前
LangGraph断点恢复与幂等执行实战
后端·架构
欧西1 小时前
AI全栈安全Agent平台(九):自动化测试用例生成引擎实战
后端