我的 asyncio 服务明明全是 async/await,QPS 却低得离谱,直到我揪出一个同步的 requests 调用,把整个事件循环死死卡住了的深度复盘
这是一个让我重新理解"异步到底异步在哪"的故事。我用 asyncio + FastAPI 写了一个服务,接口函数全是 async def,每一处该 await 的地方我都老老实实写了 await——在我看来,这就是一个"纯异步、高并发"的服务了。可上线压测,QPS 低得让我怀疑人生:明明是异步的,并发能力却跟一个老式的同步单线程服务,没什么两样。100 个并发请求打进来,它们像排队过独木桥一样,一个一个地慢慢处理,平均响应时间高得吓人。
我一开始百思不得其解:"我都写成 async 了,怎么还不并发?"我甚至怀疑是 uvicorn 的 worker 数没配对、是不是该上多进程。可在折腾部署之前,我决定先扒一扒代码。这一扒,我就找到了那个"罪魁祸首":在一个 async def 的接口函数里,我为了图方便,调用了一个同步阻塞的 requests.get() 去请求一个外部 API。就是这一行同步的、阻塞的调用,把整个 asyncio 的事件循环(event loop),给死死地卡住了——在它返回之前,事件循环什么别的事都干不了,所有其它的并发请求,都只能干等着。我这才痛苦地意识到:asyncio 的异步,是建立在"单线程事件循环 + 协程主动让出"之上的;我写了 async def,不代表里面的代码就真的"异步"了——只要我在协程里,调用了一个同步阻塞的函数(它不会"让出"控制权),那这个协程就会把整个事件循环独占、卡死,我的"异步服务",瞬间退化成了一个"伪异步、实为串行"的服务。
故障现场:一行同步的 requests,卡死整个事件循环
我把那段"看起来很异步、实则卡死循环"的代码,简化出来给你看:
import asyncio
import requests # ← 同步阻塞的库!
from fastapi import FastAPI
app = FastAPI()
@app.get("/bad")
async def bad_handler(): # ← 我写了 async def, 以为它就异步了
# ✗ 灾难: 在协程里, 调用了同步阻塞的 requests.get()
resp = requests.get("https://slow-api.example.com/data", timeout=10)
# ↑ 这一行是"同步阻塞"的: 在它返回前, 当前线程(也就是事件循环所在的线程)
# 被彻底卡住, 什么都干不了。事件循环没法去处理其它请求!
return {"data": resp.json()}
# 问题的本质:
# asyncio 是"单线程 + 事件循环"。事件循环靠协程在 await 处"主动让出",
# 来切换去处理别的任务, 从而实现并发。
# 但 requests.get() 是同步阻塞的——它不 await、不让出, 它就杵在那儿等网络。
# 于是, 整个事件循环, 被这一个请求, 死死卡住, 直到 requests 返回。
# → 100 个并发请求? 它们只能一个一个地、串行地等。异步, 名存实亡。
看着这行 requests.get(),我才算真正理解了 asyncio 的工作原理。asyncio 的并发,不是靠多线程/多进程实现的,而是靠"单线程 + 事件循环 + 协程协作"实现的:整个程序(默认)跑在一个线程里,有一个"事件循环"在不停地转;当一个协程执行到 await(一个真正异步的、会"让出"的操作,比如异步 IO)时,它会主动把控制权"让出"给事件循环,事件循环就趁这个空档,去处理别的协程;等那个异步操作完成了,再回来继续。正是靠着这种"我等 IO 的时候,你去干别的"的协作,asyncio 才能用一个线程,实现高并发。而这套机制能转起来的前提,是每个协程,在遇到耗时操作时,都得"主动让出"控制权(也就是用 await 调用真正异步的操作)。可我那行 requests.get(),恰恰违背了这个前提:requests 是一个同步阻塞的库,它的 get() 方法,在等待网络返回的那几秒里,会把当前线程彻底阻塞住——它不会 await、不会让出控制权。而当前线程,正是事件循环所在的那个唯一的线程!于是,事件循环被这一个同步调用霸占住了,在 requests.get() 返回之前,它没法去处理任何其它的协程/请求。这就是为什么,我那个"全是 async"的服务,并发能力却约等于零:每来一个请求,它的 requests.get() 就把事件循环卡住几秒,这几秒里别的请求只能干等;请求们,被迫排起了长队,一个一个地串行处理。我以为写了 async def 就万事大吉,殊不知,在协程里藏了一个同步阻塞调用,等于在异步的引擎里,掺了一把沙子。
第一件事:搞懂 asyncio 是"单线程事件循环",最怕同步阻塞
定位到根源,我必须把 asyncio "单线程事件循环"的运作机制,以及它"最怕什么",彻底搞清楚:
asyncio 的运作机制, 以及它为什么怕"同步阻塞":
# asyncio = 单线程 + 事件循环 + 协程协作
# - 默认, 所有协程都跑在"一个线程"里。
# - 有一个"事件循环(event loop)"在这个线程里不停地转。
# - 协程靠在 await 处"让出"控制权, 让事件循环能去切换、处理别的协程。
# 正常的异步(好):
# 协程A: await 异步IO ──(让出)──> 事件循环去跑 协程B ──> ... ──> IO好了回到A
# ↑ 大家在等IO时都"让出", 一个线程就能并发处理一大堆任务。
# 灾难的同步阻塞(坏):
# 协程A: requests.get() ── 同步阻塞, 不让出! 线程被霸占 ──> 事件循环卡死
# ↑ 在它返回前, 事件循环动弹不得, 协程B/C/D... 全都得干等。
# 关键认知:
# 1. async def 不等于"异步执行"——它只是"定义了一个协程"。
# 协程里的代码, 如果是同步阻塞的, 那它就是同步阻塞的, 一点不异步。
# 2. 真正让它"异步"的, 是里面用 await 调用的"真异步操作"(让出控制权)。
# 3. 事件循环是单线程的——你在任何一个协程里搞"同步阻塞",
# 卡的不是你一个请求, 而是整个事件循环、所有的请求!
# 所以, asyncio 编程的第一铁律:
# 绝不要在协程(async函数)里, 调用"同步阻塞"的操作!
# (同步阻塞包括: 同步网络IO如requests、同步文件IO、time.sleep、CPU密集计算等)
原理终于清晰了。asyncio 的核心,是"单线程 + 事件循环 + 协程协作":所有协程默认跑在一个线程里,靠事件循环调度;协程在遇到异步操作时,用 await "让出"控制权,事件循环就趁机去处理别的协程——大家"等 IO 时都让出",一个线程于是能并发处理一大堆任务。而这里有几个我之前完全没理解到位的关键认知:第一,async def 不等于"异步执行"——它只是"定义了一个协程"而已;协程里的代码,如果是同步阻塞的,那它执行起来就是同步阻塞的,一点都不异步。第二,真正让协程"异步"起来的,是里面用 await 调用的那些"真正异步的操作"(它们会让出控制权)。第三,也是最致命的——事件循环是单线程的,所以你在任何一个协程里,搞了"同步阻塞",卡住的不是你这一个请求,而是整个事件循环、所有的请求!这就解释了我的惨状:我以为 async def 是一道"异步的咒语",念上了代码就自动并发了;殊不知,它只是定义了协程,而我在协程里调用的同步 requests,把整个单线程的事件循环死死卡住,让我的并发,彻底名存实亡。由此,我给自己立下了 asyncio 编程的第一铁律:绝不要在协程里,调用任何"同步阻塞"的操作——无论是同步网络 IO(requests)、同步文件 IO、time.sleep,还是 CPU 密集计算,只要它会阻塞当前线程、不让出控制权,就绝不能直接出现在协程里。
第二件事:正解——用异步库,实在不行就丢进线程池
搞懂了根因——"协程里藏了同步阻塞,卡死了单线程事件循环"——正解就清晰了:第一选择,是把同步阻塞的库,换成对应的异步库(比如把同步的 requests,换成异步的 aiohttp 或 httpx),用 await 真正地异步调用;如果某个同步阻塞的操作实在没有异步版本(比如某个只有同步接口的第三方 SDK、或一段 CPU 密集计算),那就用 loop.run_in_executor / asyncio.to_thread,把它丢到一个单独的线程池里去执行,从而避免阻塞事件循环。
import asyncio
import httpx # ← 异步的 HTTP 库(或 aiohttp)
# 正解1(首选): 把同步库换成异步库, 真正地 await
@app.get("/good")
async def good_handler():
async with httpx.AsyncClient() as client:
resp = await client.get("https://slow-api.example.com/data", timeout=10)
# ↑ await: 真正的异步! 等网络时, 它会"让出"控制权,
# 事件循环趁机去处理其它请求 → 真并发!
return {"data": resp.json()}
# 正解2: 同步操作实在没异步版本 → 丢到线程池, 别卡事件循环
import time
def sync_heavy_task(): # 一个只有同步版本的、阻塞的操作
time.sleep(3) # (假设这是某个无法避免的同步阻塞)
return "done"
@app.get("/offload")
async def offload_handler():
# asyncio.to_thread: 把同步函数丢到线程池执行, await 它的结果
result = await asyncio.to_thread(sync_heavy_task)
# ↑ 同步阻塞的活, 在"别的线程"里干, 事件循环不被卡!
return {"result": result}
# 正解3(老写法, 等价): loop.run_in_executor
# loop = asyncio.get_running_loop()
# result = await loop.run_in_executor(None, sync_heavy_task)
# 核心区别:
# requests.get(): 同步阻塞, 卡死事件循环(灾难)
# await httpx.get(): 真异步, 让出控制权(最佳)
# await to_thread(sync_fn): 同步活丢到别的线程, 不卡事件循环(次佳)
这套正解,核心都是"别让同步阻塞的代码,占着事件循环这个唯一的线程"。正解1(换异步库,首选):把同步阻塞的库,换成它的异步版本——同步的 requests 换成异步的 httpx/aiohttp,同步的文件读写换成 aiofiles,同步的数据库驱动换成 asyncpg/异步 ORM,然后用 await 去调用;这样,在等 IO 的时候,协程会真正地"让出"控制权,事件循环就能去处理别的请求,实现真正的并发。正解2(丢线程池,次佳):如果某个同步阻塞的操作,实在找不到异步版本(比如某个只提供同步接口的第三方 SDK、或一段 CPU 密集的计算),那就用 asyncio.to_thread()(或老写法 loop.run_in_executor()),把这个同步函数丢到一个单独的线程里去执行,然后 await 它的结果;这样,同步阻塞的活,在别的线程里干,就不会卡住事件循环所在的主线程了。这两个正解的核心区别在于:requests.get() 是同步阻塞、卡死事件循环(灾难);await httpx.get() 是真异步、会让出控制权(最佳);await to_thread(sync_fn) 是把同步活丢到别的线程、不卡事件循环(次佳)。我那次的错误,就是用了最差的第一种;而正确的做法,是优先换异步库,换不了就丢线程池——总之,绝不能让一个同步阻塞调用,直接杵在协程里,把整个事件循环给霸占了。
下面这张图,对比了"协程里直接同步阻塞"和"用异步库/线程池"两条路径:
这张图的对比很清楚:左边红色那条,在协程里直接调同步阻塞的 requests,当前线程被阻塞、事件循环被霸占、所有请求退化成串行;右边绿色那条,要么换成异步库用 await(等 IO 时让出、真并发),要么把实在没法异步的同步活丢到 to_thread(在别的线程跑、不卡循环)。两条路的根本分野,在于你有没有让那个耗时操作,把事件循环这个唯一的线程,给独占住。
第三件事:怎么识别"协程里藏着的同步阻塞"
填平了 requests 这个坑,我系统地排查了一遍:到底哪些操作,是"同步阻塞"的、绝不能直接出现在协程里。我整理了一份"黑名单"和"识别方法":
# 协程里"绝不能直接调"的同步阻塞操作(黑名单):
# 1. 同步的网络IO
# ✗ requests.get/post ✓ 换 httpx.AsyncClient / aiohttp
# ✗ urllib.request ✓ 同上
# 2. 同步的文件IO
# ✗ open(...).read() 读大文件 ✓ 换 aiofiles, 或 to_thread
# 3. time.sleep() —— 经典坑!
# ✗ time.sleep(3) ← 同步阻塞, 卡死循环3秒!
# ✓ await asyncio.sleep(3) ← 异步sleep, 会让出控制权
# 4. 同步的数据库驱动
# ✗ 同步的 pymysql / psycopg2 / 同步 ORM 查询
# ✓ 换 asyncpg / aiomysql / 异步ORM(如 SQLAlchemy async)
# 5. CPU密集型计算(大循环、加解密、图像处理...)
# 它不是"等IO", 而是"真的在算"——一样会霸占事件循环!
# ✓ 丢到 to_thread / 进程池(CPU密集更适合进程池)
# 怎么识别? 几个方法:
# a. 看库: 这个库/函数是"同步"的吗? (不是 async 的、调用时不用 await 的)
# b. 看是否阻塞: 它会"等待"吗(等网络/等磁盘/等sleep)? 等待时会让出吗?
# c. 用工具: 开启 asyncio 的 debug 模式(PYTHONASYNCIODEBUG=1),
# 它会警告"协程运行时间过长"(coroutine took too long)——很可能就是阻塞了。
# d. 压测时看: 明明异步, 并发却上不去 → 大概率协程里藏了同步阻塞。
# 一句话: 协程里, 每一个"耗时"的操作, 都该问一句——
# "它是 await 调用的真异步吗? 不是的话, 它会不会卡住我的事件循环?"
这一排查,让我对"同步阻塞"有了全面的警觉。能卡死事件循环的"同步阻塞"操作,远不止 requests 一个,它们共同构成了一份协程里的"黑名单":同步网络 IO(requests、urllib → 换 httpx/aiohttp);同步文件 IO(读写大文件 → 换 aiofiles 或 to_thread);time.sleep()(这是个经典坑!很多人在协程里写 time.sleep(3),它会同步卡死循环 3 秒——必须换成 await asyncio.sleep(3));同步数据库驱动(pymysql/psycopg2 → 换 asyncpg/异步 ORM);以及 CPU 密集计算(大循环、加解密、图像处理——它不是"等 IO",而是"真的在算",一样会霸占事件循环,更适合丢到进程池)。而怎么识别协程里藏着的同步阻塞?几个方法:看库本身是不是"同步"的(调用时不需要 await 的,往往就是同步的);看它会不会"等待"且等待时不让出;用 asyncio 的 debug 模式(它会警告"协程运行时间过长");压测时观察(明明异步、并发却上不去,大概率就是协程里藏了同步阻塞)。归根结底,在写协程时,对每一个"耗时"的操作,都该习惯性地问自己一句:"它是用 await 调用的真异步操作吗?如果不是,它会不会把我的整个事件循环,给卡住?"
第四件事:CPU 密集型计算,该丢"进程池"而不是线程池
在改造的过程中,我又踩到一个更细的坑:我把一段 CPU 密集的计算(一段很重的加密/序列化),也用 asyncio.to_thread 丢到了线程池,本以为万事大吉——可它的并发,还是上不去。我这才想起 Python 的 GIL:
# 坑: CPU密集型计算, 丢"线程池"也救不了(因为 GIL)!
import asyncio, hashlib
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(): # 一段 CPU 密集的计算(纯算, 不等IO)
h = b"x" * 1000
for _ in range(5_000_000):
h = hashlib.sha256(h).digest()
return h.hex()
# ✗ 用线程池: 没用! Python 有 GIL, CPU密集的活在多线程里也是"伪并行"
# result = await asyncio.to_thread(cpu_heavy) # GIL 锁着, 多个线程也只能轮流算
# ✓ 用进程池: CPU密集要用"多进程"绕开 GIL, 才能真正并行
async def cpu_handler():
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(pool, cpu_heavy) # 在别的进程里算
return result
# 区分清楚两类"耗时":
# - IO密集(等网络/磁盘): 用异步库 await, 或 to_thread(线程池) —— 都行
# - CPU密集(纯计算): 必须用"进程池"(ProcessPoolExecutor), 才能绕开 GIL 真并行
# (线程池对 CPU密集无效, 因为 GIL 同一时刻只让一个线程执行 Python 字节码)
这个坑,让我把"耗时操作"分得更细了。同样是"耗时、不能直接卡在协程里"的操作,其实分两大类,处理方式不同:一类是 IO 密集(等网络、等磁盘、等数据库)——这类操作,CPU 其实是闲着的、在"等",所以用异步库 await、或丢到线程池 to_thread,都能解决(线程在等 IO 时会释放 GIL,别的线程能干活);另一类是 CPU 密集(纯计算,比如大循环、加解密、序列化、图像处理)——这类操作,CPU 不是在"等",而是在"真的算",而 Python 有 GIL(全局解释器锁),同一时刻只允许一个线程执行 Python 字节码,所以把 CPU 密集的活丢到线程池,根本没用(多个线程也只能被 GIL 锁着轮流算,伪并行)——它必须丢到进程池(ProcessPoolExecutor),用多进程绕开 GIL,才能真正并行。我那次,正是把 CPU 密集的活错丢进了线程池,被 GIL 卡着,自然并发上不去。把这两类耗时操作的正确处理方式,整理成一张表:
| 耗时类型 | 典型例子 | 正确做法 | 错误做法 |
|---|---|---|---|
| IO 密集 | 网络请求、读写文件、查数据库 | 异步库 await,或 to_thread 线程池 | 协程里直接同步调用 |
| CPU 密集 | 加解密、大循环、图像处理 | 进程池 ProcessPoolExecutor | 丢线程池(被 GIL 锁住,无效) |
| 定时等待 | sleep 一段时间 | await asyncio.sleep() | time.sleep()(卡死循环) |
第五件事:异步不是魔法,async def 不等于"自动并发"
这次踩坑,在认知层面给了我最大的纠偏——它打碎了我对 async 那种"加上就快"的迷信。我把这层反思,沉淀了下来:
认知纠偏: 异步不是魔法, async def 不会让代码"自动"并发
# 我的误解(错误的):
# "把函数写成 async def, 把服务跑在 asyncio 上, 它就自动高并发了。"
# → 把 async 当成了一个"加上就变快"的魔法咒语。
# 真相:
# async/await 只是一套"协作式并发"的机制——它能并发的前提, 是
# 每个协程, 在耗时的地方, 都"主动让出"控制权(用 await 调真异步操作)。
# 只要有一个协程"不让出"(同步阻塞), 整个单线程的循环, 就被它卡死。
# 所以, 异步编程, 其实需要你"全链路"都是异步的:
# - HTTP 客户端: 异步的(httpx/aiohttp)
# - 数据库驱动: 异步的(asyncpg/异步ORM)
# - 文件IO: 异步的(aiofiles)或丢线程池
# - 任何同步阻塞: 都要换异步, 或丢到 executor
# → 异步是"传染"的: 一处同步阻塞, 就能拖垮整条异步链路。
# 更深的一课: 用一个技术前, 先搞懂它的"工作原理和适用边界"
# - 我没搞懂"asyncio 是单线程事件循环、最怕同步阻塞", 就用它,
# 于是写出了"伪异步"的代码, 还以为是别的问题。
# - 理解了原理, 才知道"什么能做、什么是雷区", 才能用对、用好。
核心: 别迷信"异步=快"。异步是一套有前提、有约束的并发机制;
用对了(全链路异步)它很强, 用错了(掺了同步阻塞)它还不如同步。
这层反思,是这次踩坑给我最高维度的收获。复盘我最初的误解——"把函数写成 async def、把服务跑在 asyncio 上,它就自动高并发了"——我发现,自己是把 async,当成了一个"加上就变快"的魔法咒语。可真相是:async/await 只是一套"协作式并发"的机制,它能并发的前提,是每个协程都在耗时的地方"主动让出"控制权;只要有一个协程"不让出"(同步阻塞),整个单线程的事件循环,就会被它卡死。由此,我领悟到异步编程的一个关键特性:它需要你"全链路"都是异步的——HTTP 客户端要异步、数据库驱动要异步、文件 IO 要异步,任何一处同步阻塞,都要换成异步或丢到 executor;换句话说,异步是"传染"的:一处同步阻塞,就能拖垮整条异步链路。而这件事,给我最深的一课,其实超越了 asyncio 本身:用一个技术之前,一定要先搞懂它的"工作原理和适用边界"。我当初,正是没搞懂"asyncio 是单线程事件循环、最怕同步阻塞"这个根本原理,就稀里糊涂地用了它,于是写出了"伪异步"的代码,还把锅甩给 worker 配置、甩给该不该上多进程。直到我理解了它的原理,才知道"什么能做、什么是雷区",才能真正用对、用好它。所以,归根结底:别迷信"异步=快"——异步是一套有前提、有约束的并发机制,用对了(全链路异步),它很强;用错了(掺了同步阻塞),它甚至还不如老老实实的同步。技术没有魔法,每一份"高性能"的背后,都有你必须先理解、并严格遵守的原理和约束。把"对 async 的迷信"和"正确的认知"对比成一张表:
| 维度 | 迷信(错误认知) | 正确认知 |
|---|---|---|
| async def 的作用 | 加上就自动并发 | 只是定义协程,不保证异步 |
| 并发的前提 | 无,跑在 asyncio 上即可 | 每个协程都要主动让出控制权 |
| 同步阻塞的后果 | 顶多慢一点 | 卡死整个单线程事件循环 |
| 链路要求 | 接口写 async 就行 | 全链路异步,一处同步即传染 |
| 用前提 | 会写语法就能用 | 先懂原理和适用边界 |
一套"在协程里遇到耗时操作该怎么办"的决策流程
把这次踩坑的全部教训,我浓缩成了一张"在 async 协程里,遇到一个耗时操作,该怎么处理"的决策图,贴在了团队的异步编程规范里:
这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:协程里遇到耗时操作,先分清它是 IO 还是 CPU——如果是 IO(等网络/磁盘/数据库),优先找异步库用 await(首选),没有异步库就用 to_thread 丢线程池;如果是 CPU 密集(纯计算),就用 ProcessPoolExecutor 进程池绕开 GIL;如果只是定时等待,永远用 await asyncio.sleep(),绝不用 time.sleep()。顺着这条路走,无论哪种耗时操作,都不会把事件循环卡住,真正的并发就有了保障。这张图,现在是我们团队每个写 asyncio 服务的人,都要先过一遍的"checklist"。
我立下的几条 asyncio 编程规矩
这次"伪异步"的踩坑,让我把 asyncio 编程的注意事项,认真地立成了几条规矩:
- 绝不在协程里调用同步阻塞操作。这是第一铁律。同步网络 IO、同步文件 IO、
time.sleep、CPU 密集计算——只要它会阻塞当前线程、不让出控制权,就绝不能直接出现在协程里。 - 优先用异步库,全链路异步。HTTP 用
httpx/aiohttp,数据库用asyncpg/异步 ORM,文件用aiofiles——异步是传染的,一处同步阻塞就拖垮整条链路。 - 实在没异步版本,就丢 executor。同步 IO 丢线程池(
to_thread),CPU 密集丢进程池(ProcessPoolExecutor)——别让它们直接卡在协程里。 - 记牢
time.sleepvsasyncio.sleep。协程里要等待,永远用await asyncio.sleep();time.sleep()会同步卡死整个循环。 - CPU 密集别迷信线程池。Python 有 GIL,CPU 密集的活丢线程池没用,必须用进程池才能真并行。
- 用工具辅助排查。开
PYTHONASYNCIODEBUG=1,它会警告"协程运行时间过长";压测发现"明明异步并发却上不去",第一时间怀疑协程里藏了同步阻塞。 - 用一个技术前,先懂它的原理和边界。我这次的根源,就是没搞懂 asyncio 是单线程事件循环、最怕同步阻塞,就盲目用了它。理解原理,才能用对、用好。
写在最后
这次"我的 asyncio 服务全是 async,QPS 却低得离谱,最后揪出一个同步 requests 卡死了事件循环"的经历,是我在异步编程路上,一次很打脸、却也很受用的成长。它教给我的,远不止"协程里别用同步库"这一条技术经验,更是一种对待技术的根本态度——技术没有魔法。async 不是一道"加上就变快"的咒语,它是一套有明确前提(协程要主动让出)、有明确约束(单线程、最怕同步阻塞)的协作式并发机制。你只有先理解了它的工作原理和适用边界,才能真正驾驭它;否则,你写出的,只会是"看起来很美、实则名存实亡"的伪异步代码。
所以,当你下次用一个新技术、一个新框架的时候,请别只满足于"会写它的语法"——而要花点时间,真正搞懂它底层是怎么运作的、它最适合什么场景、它最怕什么。就像 asyncio,你只有懂了它"单线程事件循环、靠协程协作让出"的本质,才会本能地警惕"同步阻塞"这个最大的雷区;否则,你就会像当初的我一样,在异步的引擎里,稀里糊涂地掺进一把同步的沙子,然后对着低得离谱的 QPS,百思不得其解。理解原理,是用好任何一个工具的、最坚实的地基。愿你写的每一个 async,都名副其实地异步;也愿你我,在用一个技术之前,都能先沉下心,把它的原理,搞懂。共勉。
—— 别看了 · 2026