AIHub

并发与性能:进程线程协程、锁与压测

进阶22 分钟读完2026-08-06#后端#并发#性能优化#压测
并发与性能:进程线程协程、锁与压测

开篇:一家只有一口锅的餐厅

想象你开了一家餐厅。刚开始只有一位厨师、一口锅,一次只能炒一道菜,客人排队等位,大家还能忍受。突然某天上了美食推荐榜,一百位客人同时涌入——出菜速度没变,排队直接爆炸。

你有三条路:多请几位厨师(多进程);让一位厨师同时照看多口锅、哪口锅空了就炒哪口(多线程 / 事件循环);或者让厨师在等汤烧开的间隙去切菜(协程式的协作切换)。后端性能优化的所有故事,本质上都是这三条路的排列组合。

上一篇我们搞定了数据库内部的性能,这一篇把视角拉回应用层:你的代码如何同时服务成千上万个请求,以及——更重要的——如何测量它到底能服务多少。

进程、线程、协程:三种"干活的人"

进程、线程与协程的对比示意

**进程(Process)**是操作系统分配资源的最小单位。每个进程有独立的内存空间,就像独立包装的餐厅分店:互不影响,一家着火了别家照常营业,但开分店成本极高——光房租(内存)就要一大笔。进程间通信要走管道、Socket 等操作系统提供的机制,慢且麻烦。

**线程(Thread)**是 CPU 调度的最小单位,属于某个进程,共享进程的内存。它像同一家店里的多位厨师:共用厨房(内存),启动便宜,沟通直接(喊一嗓子就行),但共用一个灶台就会抢——这就是共享状态问题的来源。

**协程(Coroutine)**是用户态的"轻量线程",切换不经过操作系统内核,由程序自己决定何时让出。Node.js 的 async/await、Go 的 goroutine 都是这个思路。它像一位极其会利用碎片时间的厨师:等汤开的 30 秒绝不干站着,立刻去切菜。代价是——协程之间如果同时切同一块菜板上的菜,照样会乱。

一句话记忆:进程隔离资源,线程共享内存,协程在用户态调度。三者不是替代关系,而是不同粒度上的并发工具。

共享状态与锁:并发 Bug 的重灾区

只要多个执行流碰同一份数据,麻烦就来了。经典的"读-改-写"竞态:

// 库存扣减的错误示范
let stock = 1;

async function buy() {
  if (stock > 0) {            // 两个请求同时通过检查
    await sleep(10);          // 模拟数据库往返
    stock = stock - 1;        // 库存被扣成 -1,超卖了
  }
}

解决思路有三层。互斥锁(Mutex):给临界区上锁,同一时刻只允许一个执行流进入,像厕所门上的"有人/无人"牌子。Java 有 synchronized,Go 有 sync.Mutex,Node.js 单线程模型下更多是把这类操作推给数据库的原子语句(UPDATE stock SET count = count - 1 WHERE count > 0)。

原子操作:CPU 或数据库层面保证"不可分割"的操作,比如 Redis 的 INCR、数据库的原子 UPDATE。它们不需要你手动加锁,是性能最好的方案,能用原子操作就别用锁

无锁结构:CAS(Compare-And-Swap)等机制,通过"先比较再交换"的硬件指令避免加锁。理解它即可,业务代码里极少手写。

记住一条经验法则:锁的粒度越小越好,持锁时间越短越好。锁里绝不放 I/O 操作(网络请求、磁盘读写),否则一个慢请求会拖死所有排队的人。

池化:为什么连接池和线程池是标配

创建资源是有成本的。建立一个数据库连接要经过 TCP 握手、认证、分配缓冲区等步骤,耗时几到几十毫秒——如果每个请求都新建连接,高并发下光是建连就把数据库压垮了。

连接池的做法是:预先建好一批连接放在池子里,请求来了借一个,用完还回去。Node.js 里 pg.Pool、Java 里 HikariCP 都是这个模式:

import pg from 'pg';
const pool = new pg.Pool({ max: 20 }); // 最多 20 个连接

const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
// 用完自动归还,不要 await client.end()

线程池同理:预先创建固定数量的工作线程,任务排队执行,避免无限制创建线程把内存和 CPU 调度压垮。

池化的本质是用排队换稳定:宁可让请求在池子前排队,也不让后端资源被瞬间击穿。池的大小不是越大越好——数据库连接数超过它的处理能力,只会让所有查询一起变慢。

QPS、RT、吞吐量:建立性能直觉

谈性能先说清三个指标。RT(Response Time,响应时间):单个请求从发出到收到结果的耗时,用户直接感知的是它。QPS(Queries Per Second):每秒处理的请求数。吞吐量:单位时间处理的工作量,和 QPS 类似但可用于数据量(MB/s)。

它们的关系常被误解。一个常见直觉错误是"RT 翻倍,QPS 就减半"——只在串行处理时成立。系统有并发度 C 时,近似关系是:

QPS ≈ 并发度 / 平均RT(秒)

例如 RT 是 100ms,应用同时能处理 50 个请求,理论 QPS 约 500。这个公式(利特尔法则的简化版)告诉你两件事:降 RT 和提并发度都能涨 QPS,但并发度受限于线程池、连接池、CPU 核数,RT 往往才是杠杆更大的那个

上下文切换:并发不是免费的午餐

多线程为什么不是越多越好?因为 CPU 在多个线程间切换时需要保存和恢复现场(寄存器、栈指针、缓存局部性),这个动作叫上下文切换,每次约几微秒。听起来很快,但当成千上万个线程疯狂轮转时,CPU 相当一部分时间都花在"换人"而不是"干活"上——就像一位厨师在十口锅之间来回跑,大部分体力耗在了走路上。

这解释了工程实践中的两条结论:CPU 密集型任务,线程数开到核数附近(核数 +1 是经典经验值)就到顶了;I/O 密集型任务,线程大部分时间在等网络和磁盘,可以开多一些(常见公式是 核数 × (1 + 等待时间/计算时间)),这也正是 Node.js 用单线程事件循环 + 异步 I/O 就能撑起高并发的原因——它几乎不付出线程切换的代价。

压测:用数据代替猜

不要凭感觉说"我觉得能扛住"。常用工具有 wrk(命令行,高性能)、autocannon(Node 生态友好)、JMeter(图形化,功能全)。以 autocannon 为例:

npx autocannon -c 100 -d 30 http://localhost:3000/api/products
# 100 个并发连接,持续 30 秒

跑完重点看四个数:平均 RT 和 P99 RT(99% 的请求多快返回,长尾比平均值更能暴露问题)、QPS、错误数。压测的正确姿势是阶梯加压:从 10 并发开始,逐步加到 50、100、500,画出 QPS 随并发度变化的曲线。曲线的拐点——QPS 不再增长、RT 开始飙升的位置——就是系统的极限。

压测前记得排除干扰:关掉日志里的 debug 输出、确认压测机本身不是瓶颈、数据库里要有接近真实量级的数据(100 行数据上的压测毫无意义)。

性能优化方法论:先测量,再优化

这是本篇最重要的一节。新手最大的错误是凭直觉优化:怀疑某段代码慢,花两天重写,结果整体 RT 纹丝不动——因为瓶颈根本不在这儿。

正确的流程是:

  1. 建立基线:压测得到当前 QPS / RT,截图存档。
  2. 定位瓶颈:用工具看时间花在哪。Node.js 可用 --prof 或 clinic.js,慢查询看数据库的执行计划,链路长的上 APM(如 SkyWalking、Jaeger)做分布式追踪。
  3. 只改瓶颈点:一次只改一处。
  4. 重新压测对比:数据没有改善就回滚,别舍不得。

经验上,后端请求的耗时大头排名通常是:数据库查询 > 外部 API 调用 > 序列化 > 业务计算。所以"优化 for 循环"几乎从来都不是正确答案,而"给查询加索引""把 N+1 查询合并""加缓存"经常一招见效。

常见误区

  • 误区一:开更多线程 = 更快。 线程超过 CPU 核数后,上下文切换的开销会吃掉收益;连接数超过数据库承受能力同理。
  • 误区二:平均 RT 很好看就没问题。 P99、P999 的长尾才代表真实用户的倒霉体验,平均值会掩盖它。
  • 误区三:在开发环境压测出结论。 笔记本上的 SQLite 和生产环境的 PostgreSQL 集群是两回事,压测环境要尽量贴近生产。
  • 误区四:优化没有验收标准。 先写清"目标:P99 < 200ms,QPS ≥ 1000",否则优化永远做不完。

小结

并发是后端从"能跑"到"能扛"的分水岭。记住这几条主线:进程隔离、线程共享、协程轻量,按场景选择;共享状态优先用原子操作,其次小粒度锁;连接池线程池是用排队换稳定的标配;QPS ≈ 并发度 / RT 是你的心算工具;最后,一切优化始于压测数据,终于压测数据。下一篇我们进入缓存体系,看看 Redis 如何把这些性能知识变成实战武器。


系列导航:上一篇《数据库(下):B+ 树索引、事务隔离与慢查询优化》(/content/backend-expert-06-database-advanced) · 下一篇《缓存体系:Redis 数据结构到缓存三大坑》(/content/backend-expert-08-redis-cache)

相关教程

并发与性能:进程线程协程、锁与压测 | AIHub