很多人被「并发」和「并行」绕晕:一个同事说接口要支持高并发,另一个说要并行跑任务,你听着像一回事,动手时却发现排队、限流、多核跑满完全是三套东西。这篇不堆定义,先用几个日常场景把两者的边界划清,再落到工程里——什么活儿天生适合并发、什么活儿非并行不可、并发数怎么估、超时重试和降级怎么搭。
很多人被「并发」和「并行」绕晕:一个同事说接口要支持高并发,另一个说要并行跑任务,你听着像一回事,动手时却发现排队、限流、多核跑满完全是三套东西。这篇不堆定义,先用几个日常场景把两者的边界划清,再落到工程里——什么活儿天生适合并发、什么活儿非并行不可、并发数怎么估、超时重试和降级怎么搭。
先把一句话放这儿:并发是「怎么安排任务」,并行是「用几份力气同时干」。前者讨论结构,后者讨论资源。搞混这两个口径,选型就会跑偏——该加队列的地方你去加机器,该加核的地方你去加线程,钱花了效果没出来。下面逐节拆开讲。
先看两个场景:你以为说的是同一件事
第一个场景:一家小店只有一个窗口,店员一边收银一边帮顾客找货,中间还接了个电话。窗口只有一个,但店员在多个顾客之间来回切换,谁都没被彻底晾着——这是并发。第二个场景:同一家店开了三个窗口,三个店员同时各招呼一队顾客。同一时刻真的有三份力气在干活——这是并行。
| 对比项 | 单个窗口的店员 | 三个窗口的店员 |
|---|---|---|
| 同一时刻在做事的人数 | 1 个 | 3 个 |
| 任务怎么推进 | 来回切换、交错推进 | 各干各的、同时推进 |
| 换人速度够不够 | 看怎么排优先级 | 看窗口数和队伍长度 |
| 出问题时的表现 | 一个人卡住全店卡住 | 一队卡住其他队照常 |
混淆通常来自口语:日常说「同时处理很多请求」,说的其实是并发;说「多开几个进程跑快点」,说的才是并行。判断口径可以这么问:如果我把执行单元从 1 份加到 3 份,速度明显上去了,那你做的是并行;如果只是加队列、加让出,速度没大变但不再互相堵死,那你做的是并发。
并发:一个人手里同时开好几件事
并发的本质是用调度让多个任务在时间上交错推进,关键前提是任务会等待——等网络返回、等磁盘读、等上游模型出结果。等待期间把执行权让出去,别的任务就能往前挪,于是单份资源也能「看起来」在同时处理很多件事。
- 任务数可以远大于执行单元数,十个请求挤一个执行位是常态
- 瓶颈多半在等待时间,而不是算力总量,所以优化方向是减少等待和排队
- 衡量指标看的是吞吐和延迟分布,不是单个任务跑得多快
- 代价是调度开销:切得太碎、锁竞争太重,反而比顺序执行更慢
什么情况下不适用:任务是纯 CPU 计算且彼此独立时,靠交错推进并不能变快,只多出切换开销。这时候你该考虑的是并行或者批处理,而不是继续加协程。另一个不适用场景是任务之间有强顺序依赖,比如后一步必须用到前一步的完整结果,硬做并发只会把逻辑绕成一团。
并行:多双手各干各的
并行是同一时刻真的有多个执行单元在跑,需要多核、多进程、多机器这类物理条件。它不是调度技巧,而是实打实把活儿切开、分给多份资源同时做。没有多份资源,谈并行就是空话。
- 并行度受限于执行单元数:四核机器上开八个纯计算进程,多半互相抢 CPU
- 瓶颈在算力总量,以及任务能不能被干净地切开
- 切开之后要考虑结果怎么合并、顺序怎么保证、失败怎么重放
- 调试成本更高:多进程并发写的 bug 往往不重现,日志要提前设计
什么情况下不适用:任务之间有强依赖、必须按顺序拼结果时,硬拆并行只会带来一致性问题。比如一个必须严格按顺序追加的账本写入,拆成几路并行去写,最后对账能对到你怀疑人生。这类活儿该做的是排队加限速,而不是拆开。
两者的联系:并行是并发的一种实现手段
两者不是对立关系,而是层次不同。并发是「怎么安排任务」的结构问题,并行是「用几份力气同时干」的资源问题。你可以只用一份资源实现并发,比如单核上跑协程;但你没法在没有多份资源的情况下实现真正的并行。
| 维度 | 并发 | 并行 |
|---|---|---|
| 回答的问题 | 任务怎么排、怎么让出 | 同时用几份资源在跑 |
| 依赖条件 | 调度机制、任务可等待 | 多核、多进程、多机器 |
| 瓶颈位置 | 等待与排队 | 算力总量与可切分性 |
| 加资源是否变快 | 通常不会明显变快 | 通常会明显变快 |
| 典型手段 | 异步、协程、事件循环 | 多进程、批处理、分布式 |
工程里几乎总是混着用:先靠并发把大量 IO 等待的请求摊开,再把其中真正吃 CPU 的环节丢给多进程或批处理。判断口径也简单——如果加机器、加核数能明显提速,说明你在做并行;如果只是加了排队和让出,说明你在做并发。两者配合的常见姿势是「并发接客、并行算账」,中间用队列解耦。
按活儿挑:什么时候要并发,什么时候要并行
- 先给活儿分类把链路里的每一步标成等待型(调模型接口、读数据库、抓文档)还是计算型(本地推理、向量化、大文件解析)。标不出来就先别动手。
- 等待型优先并发用异步或协程把等待摊开,配好超时和并发上限。盲目开多进程反而增加内存和上下文切换成本,还要处理进程间通信。
- 计算型优先并行按核数决定并行度,不要凭空拍数字。切分粒度要合适:切太细调度开销吃掉收益,切太粗又跑不满。
- 混合型分段处理抓取阶段并发、计算阶段并行,中间用队列解耦。这样两段瓶颈互不干扰,扩哪一段就扩哪一段。
- 验证收益改完前后各测一轮,看的是端到端耗时和失败率,不是单看某个函数的耗时。到 /tools/speed-test 做一遍基线对比。
反过来的教训也要记:IO 密集型任务如果非要用多进程去扛,内存占用会先顶到天花板;CPU 密集型任务如果只靠协程,测试机看着没满,实际上算力一点没多用上。先分类,再动手。
并发数怎么定:从基线量起,而不是拍脑袋
并发数不是越大越好。它要同时满足三个约束:你自己能扛住、上游能接受、失败时能兜住。三个里任何一个被突破,调大并发数只会把问题从慢变成挂。
- 先量基线:到 /tools/speed-test 量出单次请求的延迟分布,用「目标吞吐 ÷ 单次耗时」粗算需要的并发位
- 再看上游:外部服务普遍有并发或频率上限,超过就被拒或被排队,所以并发数要和退避、排队策略一起设计
- 留出余量:把并发位定在你实测能承受的峰值之下,突发流量交给队列缓冲,比事后救火便宜
- 动态调整:按排队时长和失败率做反馈,排队变长就减位,上游变快再加位,别写死一个常数
不同服务的上限差异很大,具体数值到 /price-watch 和各家官方文档对应的站内页面核对当时的数字,别照抄别人博客里的配置。上面这些只是估算方法,真实值要靠压测和观察得出。
配套的三件套:超时、重试、降级
并发和并行调好了,不等于链路稳。真正让线上出事的是超时没设、重试乱来、降级没预案。这三件事要跟并发数一起设计,分开了做必然对不上。
- 连接超时
- 建立连接阶段的等待上限,通常设得短,用来快速发现对方不可达
- 读取超时
- 连接建立后等响应的上限,要覆盖上游正常的最慢耗时
- 整体超时
- 一次请求从头到尾的总上限,必须小于上游给你的容忍时间
- 指数退避
- 重试间隔逐次拉长并加随机抖动,避免同一时刻一起重试
- 幂等性
- 判断一个失败能不能重试的前提,非幂等接口重试可能造成重复计费或重复写入
- 降级动作
- 失败后走哪条路:换备用通道、换更小的模型、返回缓存或明确报错
重试只对幂等且可恢复的失败有意义,且必须带上限和退避,否则一次上游抖动会被你的重试放大成雪崩。降级顺序先想清楚:换备用通道、降级到更小的模型、直接返回缓存结果或明确失败,别让请求无限挂在那里。失败要快速、要可见,别静默吞掉。
上线前照着过一遍的清单
下面这几条不复杂,但漏掉任何一条都可能在流量高峰时变成事故。上线前挨着过一遍,比事后翻日志猜原因省事得多。
- 画一张调用链路图,标清哪一段是等待型、哪一段是计算型,并发与并行的位置一眼能看懂
- 给每个外部依赖记四个值:并发上限、超时值、重试次数、失败后的降级动作,缺一项就先补上
- 日志里留下并发位、排队时长、实际耗时和失败原因,出问题时能一眼看出是排队堵了还是上游慢了
- 对压测和线上都记录峰值并发与失败率,改配置前后各留一份对照
- 把降级开关做成可远程切换的,出事时不用重新发版
如果团队同时用到多个中转站或官方通道,还要把「切通道」纳入清单:哪条主、哪条备、切换的判断条件是什么。这块可以在 /sites 上看各家的说明和实测情况,再到 /rankings 对照当前状态,别等挂了才临时挑。
并发是排队艺术,并行是分工艺术;先把任务分类,再决定是排还是分。下一步把链路里每段是等待型还是计算型标出来,用站内的延迟基线估算并发位,再补齐超时、重试和降级三项配置。对照 /tools/speed-test 的数字和 /price-watch 的上游限制,能帮你把并发数定在既跑得动又兜得住的位置。→
常见问题
并发和并行是不是一回事?
不是。并发讲的是任务怎么被安排、在等待时怎么让出执行权,一份资源也能做;并行讲的是同一时刻真的有多个执行单元在跑,需要多核或多机器。并行可以看作并发的一种实现方式,反过来不成立。
多线程就等于并行吗?
不一定。在核数不够或任务主要在等网络时,多线程更多是在做并发切换,同一时刻真正同时在算的只有一个。想确认是否真的并行,看加了核数之后总耗时有没有明显下降。
调用大模型接口,并发数是不是越大越好?
不是。上游通常有并发或频率限制,调太大很快会被拒或被排队,还会拉高整体延迟。正确做法是先量延迟基线,再按目标吞吐反推并发位,并配好退避、排队和降级。
任务跑得慢,是该加并发还是改并行?
先看慢在哪。大部分时间在等外部返回,属于等待型,加并发位通常有效;大部分时间在本地计算,属于计算型,得靠多进程或多机器并行。判错了方向,加了资源也不见快。
并发上去之后反而更慢了,先查什么?
先查是不是排队和重试在打架:下游限流触发了重试,重试又占了本该给正常请求的并发位。再看日志里的排队时长和失败率,把并发位和退避策略一起收一收通常就能缓过来。