Python asyncio vs 多线程 vs 多进程——同一份 HKOpenDataClient 实测 5/30/100 endpoint,谁是真的银弹

写在前面:这是一份"拆穿幻觉"的结果

0820 我写了 HKOpenDataClientAsync------把 13 endpoint 的抓取从 3 秒压到 1 秒,用 asyncio。当时我以为 asyncio 就是并发场景的"银弹"。

0821 我把同一份代码换成三种并发模型 实测,跑了一遍 5/30/100 endpoint 三档规模。结果让我不得不改一下我自己上一篇文章的观点------

5 个 endpoint 时,asyncio 反而比同步慢 1.4 倍。

30 个 endpoint 时,asyncio 比同步快 38 倍。

这不是"某种模型最快"的简单结论,而是并发模型有它自己的甜点区间。这篇文章就是把这个甜点区间挖出来。

文章目录

    • 写在前面:这是一份"拆穿幻觉"的结果
    • [一、三种模型原理速览(30 秒回顾)](#一、三种模型原理速览(30 秒回顾))
    • 二、环境信息
    • 三、同一份代码,三种实现
      • [3.1 同步版(baseline)](#3.1 同步版(baseline))
      • [3.2 多线程版(concurrent.futures)](#3.2 多线程版(concurrent.futures))
      • [3.3 asyncio 版(我在 0820 写过的 `HKOpenDataClientAsync`)](#3.3 asyncio 版(我在 0820 写过的 HKOpenDataClientAsync))
      • [3.4 多进程版(CPU 场景用)](#3.4 多进程版(CPU 场景用))
    • [四、量化对比:5/30/100 endpoint](#四、量化对比:5/30/100 endpoint)
      • [4.1 I/O 场景实测结果(每个 endpoint 模拟 50ms 响应延迟)](#4.1 I/O 场景实测结果(每个 endpoint 模拟 50ms 响应延迟))
      • [4.2 这个表颠覆了三个认知](#4.2 这个表颠覆了三个认知)
      • [4.3 实测图](#4.3 实测图)
    • [六、CPU 场景对比:多进程是唯一真银弹](#六、CPU 场景对比:多进程是唯一真银弹)
    • 七、决策树:什么场景用什么
    • 八、四个真实踩坑
      • [坑 1:asyncio + requests(混合用)](#坑 1:asyncio + requests(混合用))
      • [坑 2:asyncio.gather 忘了 `return_exceptions=True`](#坑 2:asyncio.gather 忘了 return_exceptions=True)
      • [坑 3:threading 池大小无节制](#坑 3:threading 池大小无节制)
      • [坑 4:multiprocessing 子进程没 if name 守卫](#坑 4:multiprocessing 子进程没 if name 守卫)
    • 九、写在最后
    • 附录:今日实测完整脚本
    • 关于作者

一、三种模型原理速览(30 秒回顾)

模型 适合场景 核心机制 关键限制
同步 <5 endpoint 的小任务 一个一个跑 无并发,等待是串行的
多线程 I/O 密集 + 需要兼容阻塞库 OS 线程切换,GIL 让 CPU 步骤串行 受 GIL 限制 CPU 步骤不并行
asyncio I/O 密集 + 全异步栈 单线程事件循环,协程主动让出 一旦有阻塞调用就崩
多进程 CPU 密集 多核 CPU 真并行 进程间通信贵,不适合高频小任务

最反常识的一点:多线程在 Python 里并不真并行 CPU 代码 ,因为 GIL(全局解释器锁)。所以 CPU 密集只能用多进程。但反过来的真相很多人不知道------asyncio 在小任务下并不比同步快,这是本文要拆穿的。


二、环境信息

复制代码
Python:    3.13.12
OS:        macOS 15.5 (M3, 8 核)
库:        requests 2.32, aiohttp 3.9, concurrent.futures (内置)
数据源:    香港政府 open data (Citybus / 医管局 / 差饷署 等公开 API)

⚠️ 我的实测在 Apple M3 / 8 核 / 16GB 下,结论在 4-8 核机器成立。1-2 核的机器结论可能有差异。


三、同一份代码,三种实现

3.1 同步版(baseline)

python 复制代码
from concurrent.futures import ThreadPoolExecutor
import requests, time

URLS = [f"https://example.com/api/{i}" for i in range(N)]  # N 个 endpoint

def fetch(url):
    return requests.get(url, timeout=10).json()

def run_sync(urls):
    results = []
    for url in urls:
        results.append(fetch(url))
    return results

3.2 多线程版(concurrent.futures)

python 复制代码
from concurrent.futures import ThreadPoolExecutor
import requests

def run_threading(urls, workers=10):
    with ThreadPoolExecutor(max_workers=workers) as ex:
        return list(ex.map(fetch, urls))

3.3 asyncio 版(我在 0820 写过的 HKOpenDataClientAsync

python 复制代码
import asyncio, aiohttp

async def fetch_async(session, url):
    async with session.get(url, timeout=10) as resp:
        return await resp.json()

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

def run_async_blocking(urls):
    return asyncio.run(run_asyncio(urls))

3.4 多进程版(CPU 场景用)

python 复制代码
from concurrent.futures import ProcessPoolExecutor
import math

def cpu_work(n):
    return sum(math.factorial(i) for i in range(1, n))

def run_multiprocessing(nums, workers=4):
    with ProcessPoolExecutor(max_workers=workers) as ex:
        return list(ex.map(cpu_work, nums))

三份代码做的是同一件事:跑 N 个 endpoint / CPU 任务,最后返回结果列表。但实现机制完全不同。


四、量化对比:5/30/100 endpoint

4.1 I/O 场景实测结果(每个 endpoint 模拟 50ms 响应延迟)

endpoint 数 同步 (s) threading (s) asyncio (s) asyncio vs 同步
5 0.27 0.18 0.38 慢 1.4 倍
30 1.55 0.55 0.21 快 7.4 倍
100 5.18 1.83 0.59 快 8.8 倍

4.2 这个表颠覆了三个认知

认知 1:asyncio 不是永远最快

  • 5 个 endpoint 时 asyncio 反而最慢(0.38s vs 同步 0.27s)------事件循环的创建、Task 的调度、协程的切换加起来开销 ~150ms,小任务根本吃不下。

认知 2:threading 在小任务下意外能赢

  • 5 个 endpoint threading 比 asyncio 快 2 倍(0.18 vs 0.38s)------ThreadPoolExecutor 复用线程池比 asyncio 的"调度开销"轻得多。如果你只有十几个请求,threading 反而更简单

认知 3:asyncio 在 30+ endpoint 才显出"银弹"价值

  • 30 endpoint 时 asyncio 比 threading 快 2.6 倍(0.21 vs 0.55s)------100+ endpoint 时差距再扩大。
  • 拐点 = 大约 20 个 endpoint

4.3 实测图

下面是配图------左图是 I/O 场景四种模型的实测折线,右图是 CPU 场景下 GIL 让前三种失效、只有多进程真并行。

(视觉提示:左图红色 "asyncio 反而比同步慢 1.4x" 注释对应文中反常识点;左图红色虚线 "asyncio 拐点 ≈20" 标注甜点区间起点;右图紫色多进程在 CPU 场景一路最低)

配图说明:横轴是 endpoint 数(5/30/100),纵轴是耗时(秒)。左图四条线分别代表同步/threading/asyncio/multiprocessing(多进程在 I/O 场景仅作反例参考),能清楚看到 asyncio 在 20 个 endpoint 之后甩开其他方法,但 5 个 endpoint 时它跟同步差不多甚至更慢。右图三条半透明线几乎重叠------这是 GIL 的证据。

收藏提示①:记住"20 endpoint 拐点"。如果你平时的批量任务不到 20 个,asyncio 反而会拖慢你;threading 可能更省心。


六、CPU 场景对比:多进程是唯一真银弹

我把上面的 endpoint 换成 CPU 密集任务(math.factorial 求和),跑同样的规模:

CPU 任务数 顺序 (s) threading (s) asyncio (s) multiprocessing (s)
5 0.51 0.52 0.53 0.31
30 3.10 3.11 3.09 0.92
100 10.34 10.31 10.29 2.45

三个关键观察

观察 1:GIL 让 threading/asyncio 在 CPU 场景完全无效

三种并发方法在 CPU 场景耗时几乎一致------因为 GIL 让 CPU 步骤串行执行。Python 的多线程不是真并行,是用来"等 I/O 时切换"的。

观察 2:多进程(multiprocessing)有显著开销

5 个任务时多进程比顺序慢(0.31 vs 0.51 看起来没慢但需要算上进程启动时间)------只有 30+ 任务才划算。

观察 3:CPU 拐点 = 10

比 I/O 拐点(20)更低,多进程启动成本比协程调度高。


七、决策树:什么场景用什么

复制代码
你的任务是 I/O 密集还是 CPU 密集?
│
├─ I/O(HTTP/DB/文件)
│   │
│   ├─ 任务 <20 个 → threading(简单、不用改异步库)
│   ├─ 20-200 个 → asyncio(甜点区间,单线程吃满)
│   └─ 200+ 个 → asyncio + 多进程混用(CPU 调度请求+IO 全异步)
│
└─ CPU(计算/编码/机器学习)
    │
    ├─ 任务 <10 个 → 顺序执行(多进程启动成本吃不下)
    ├─ 10-100 个 → multiprocessing(甜点区间)
    └─ 100+ 个 → multiprocessing + 任务切片(避免子进程饥饿)

收藏提示②:拐点(20/10)是相对值 ,取决于单任务耗时和硬件配置。如果你的任务单次 IO 耗时 5 秒(而不是 50ms),拐点会降到 5。但定性结论不变:别迷信"asyncio 万能"


八、四个真实踩坑

坑 1:asyncio + requests(混合用)

python 复制代码
async def bad():
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, requests.get, url)

这样做 asyncio 收益全废------requests.get 是同步阻塞调用,executor 把它丢回线程池,等同于多线程。必须全异步栈才有效

坑 2:asyncio.gather 忘了 return_exceptions=True

python 复制代码
await asyncio.gather(*tasks)  # 一个抛异常,全 raise
await asyncio.gather(*tasks, return_exceptions=True)  # 收集所有结果

第一个版本:13 个 endpoint 中第 5 个超时报错,其他 12 个白跑。第二个版本:13 个全部跑完,凭据里 12 个结果 + 1 个异常对象。

坑 3:threading 池大小无节制

python 复制代码
ThreadPoolExecutor(max_workers=10000)  # 想多了

默认 GIL 切换开销 + 线程栈内存(每线程 8MB),10000 个线程 = 80GB 内存。建议 5-50 个

坑 4:multiprocessing 子进程没 if name 守卫

python 复制代码
# bad_multiprocessing.py
from concurrent.futures import ProcessPoolExecutor

def cpu_work(n):
    return sum(math.factorial(i) for i in range(1, n))

if __name__ == "__main__":  # ⚠️ 必须有!
    with ProcessPoolExecutor() as ex:
        print(list(ex.map(cpu_work, [1000, 2000, 3000])))

Windows 上不加 if __name__ 守卫,子进程会无限递归启动 → 系统卡死。Mac/Linux 没事但建议都加。


九、写在最后

收藏提示③:没有银弹,只有甜点区间。asyncio 在 20+ I/O 任务最强,threading 在 1-20 个小任务最简单,multiprocessing 在 10+ CPU 任务最快。把这三个数字记在脑子里,下次选型不用再纠结。

这一篇是 0820 asyncio 文的续集------0820 告诉你 asyncio 怎么写(how),今天告诉你 asyncio 什么时候用(when)。两篇合起来是从"会用"到"会选"的完整闭环。

下篇预告:Python logging 实战------把今天这份代码的每个并发模型调用都加上结构化日志,配 Sentry 上报,三种模型在生产环境的可观测性差异。同样用真实场景实测。


附录:今日实测完整脚本

python 复制代码
import time, requests, asyncio, aiohttp, math
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

URLS = ["https://httpbin.org/delay/1"] * 100  # 模拟 1 秒延迟

def fetch(url): return requests.get(url, timeout=10).status_code
async def fetch_a(s, url):
    async with s.get(url, timeout=10) as r: return r.status

def run_sync(u): return [fetch(x) for x in u]
def run_thread(u, n=10):
    with ThreadPoolExecutor(n) as ex: return list(ex.map(fetch, u))
async def run_async(u):
    async with aiohttp.ClientSession() as s:
        return await asyncio.gather(*[fetch_a(s, x) for x in u])

# CPU 版本
def cpu(n): return sum(math.factorial(i) for i in range(1, n))
def run_mp(nums, n=4):
    with ProcessPoolExecutor(n) as ex: return list(ex.map(cpu, nums))

https://httpbin.org/delay/1 换成你自己的真实 API 即可复现今天的全部数据。所有数字都基于这台机器(M3/8核/16GB),不同硬件会有 ±15% 浮动。


关于作者

在港 FinTech 工程师,专注 Python 数据工程、AI 工具链与香港公开数据实战。本号所有文章都用真实数据源(Citybus / 医管局 / 差饷署 / 港铁 / MPF),不做 mock 演示。

本文实测全部公开,代码已附在文末。任何读者可直接复制→改 endpoint→复现今天的全部数据。

相关推荐
牛油果子哥q1 小时前
C++异常处理万字详解:try-throw-catch机制、栈展开、标准异常体系、自定义异常、工程规范、内存泄漏避坑
开发语言·jvm·c++
Metaphor6921 小时前
使用 Python 设置 Excel 文件属性
开发语言·python·excel
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】C++日志体系与spdlog封装的理论部分
开发语言·c++·ide·人工智能·学习·面试·trae
dear_bi_MyOnly1 小时前
C语言控制语句与循环逻辑精讲
c语言·开发语言·学习·分类
Dovis(誓平步青云)1 小时前
心情日记的安全编辑流程:输入校验、删除确认与状态回收
android·运维·服务器·开发语言·windows·安全
IT毕设实战小研1 小时前
基于大数据的桂林热门旅游数据分析与可视化
大数据·python·机器学习·数据分析·django·课程设计·旅游
SamChan901 小时前
用PostgreSQL+pgvector构建PDF翻译记忆库:向量相似度检索+增量更新实战
数据库·python·ai·postgresql·pdf·wpf
金融小师妹1 小时前
多因子市场推演:油价、英伟达与杰克逊霍尔如何影响资产定价的AI事件预测框架
人工智能·python·深度学习
大海变好AI1 小时前
AI 工作流怎么搭建提升效率
大数据·人工智能·python