深夜十二点,一家大餐厅的后厨依然灯火通明。几十张订单贴在出菜单上,穿着白色制服的帮工们各司其职——有人专管切墩,有人守着炒锅,有人负责摆盘。没有人闲逛,也没有人扎堆抢一个灶台。这套行云流水的协作,靠的不是运气,而是一套精心设计的“调度系统”。 ONNX Runtime 的多线程推理,就是深度学习模型的“后厨调度系统”。模型好比一本菜谱,每个算子好比一道工序,而线程池就是那群训练有素的帮工。它们互相配合、并行作业,让模型在服务器上以最短时间跑完一轮推理。这篇文章会带你从零认识这套系统:线程池怎么建、任务怎么分、性能怎么调,以及真实项目里踩过的那些坑。
对 AI 从业者而言,多线程推理是从“模型能跑”迈向“生产可用”的必经之路。理解 ONNX Runtime 的线程模型,你才能真正榨干 CPU 的每一分算力,让推理服务扛住高并发流量。
目录
可视化演示
三步展示多线程推理:基于任务依赖图的拓扑调度、粗粒度(算子间)/细粒度(算子内)并行粒度、以及intra_op/inter_op_num_threads线程数配置。
本节配有交互动效(进入视口后自动加载,约 3KB)。
任务依赖图调度 A/B/C: 算子任务节点 拓扑序:依赖顺序执行 节点=算子,边=数据依赖 线程池按拓扑序调度并行任务。
线程池机制:稳固的后厨帮工团队
后厨不会等客人点完菜才跑去人才市场雇人,而是常年养着一支固定的帮工团队——来单就干,没单待命。ONNX Runtime 也遵循同样的智慧:它在线程池里预先创建好一批工作线程,模型推理时只管往“出菜单”上挂任务,线程们自会认领。
线程池按职责分为两类。第一类是 IntraOp 线程池,负责“算子内并行”——就像切墩师傅把一颗白菜分成四份,四人同时切。一个矩阵乘法算子可以把输出矩阵切成若干块,分给池内多个线程同时计算。第二类是 InterOp 线程池,负责“算子间并行”——就像炒锅组里,A师傅炒宫保鸡丁,B师傅同时炒麻婆豆腐,只要两个算子互不依赖,就可以并行推进。
任务队列是这套系统的枢纽。图执行器按依赖关系扫描计算图,把可并行的节点投入队列,线程池从队列头部取任务执行。队列模型决定了公平性与延迟上下限。
正是在这种“预建团队 + 队列分发”的机制下,ONNX Runtime 避免了频繁创建线程的开销,也让每一颗 CPU 核心都像后厨的灶台一样被充分使用。理解了线程池的双池结构,你就抓住了整个多线程推理的地基。
- N_{threads}(线程池实际线程数)决定后厨同时开几个灶,最终参与干活的人数。
- min(取最小值)就像后厨不会让十个帮工去抢两个灶台,而是让多余的人待命,避免空转损耗。
- N_{cores}(物理核心数)电脑 CPU 真正能同时干活的“灶台数”,超线程另算,但物理核是硬指标。
- N_{tasks}(可并行任务数)当前菜谱中能拆开的工序数。如果只有一道菜,人再多也帮不上忙。
并行执行模式:多口锅同时开火
一支帮工团队在手,怎么把活派出去才能最快出菜?后厨有三种典型的并行打法,ONNX Runtime 的并行执行模式与之如出一辙。
数据并行对应“同一道菜多锅同时炒”——模型输入是多张图片时,把一批图片切分成多份,每份由一个线程走完整的计算链。这依靠 IntraOp 线程池发力。任务并行对应“不同菜品分头做”——计算图中互不依赖的分支算子,分别交给 InterOp 线程池里的不同线程,各算各互不打扰。流水线并行则更有意思:像传菜流水线,样本 A 的卷积还没算完,样本 B 已开始前处理,样本 C 已进入下一步,三个样本在时间轴上交错推进,让每个算子都保持忙碌。
不管哪种模式,都有一个“同步与等待”的问题——菜必须全部出锅才能上齐。执行引擎在汇合点等待最慢的线程完成,所以并行越快,木桶效应越明显。这正好引出了 Amdahl 定律:
回到后厨,哪怕是世界上最优秀的帮工团队,也要等最后一道菜出锅才能上席。这就是并行执行的宿命——识别模型里哪些环节是“不可并行的独灶”,才是优化的真正起点。
- S(加速比)使用多线程后,推理速度相对单线程提升的倍数。S=4 表示快了 4 倍。
- p(可并行代码占比)推理过程中能分给多线程干的比例。p=0.9 就是 90% 的工作可以并行。
- 1-p(串行比例)必须排队逐份完成的部分,比如最终结果汇总、线程同步等待。
- N(线程数)参与并行的线程数量。N 越大,p/N 越小,但永远不会让分母归零。
图优化调度:菜谱流程的重排
有了好团队,菜谱本身也得合理。大厨拿到食材,不会机械地照着菜谱一步一步来,而是会琢磨:哪两步能合并?哪步可以提前做?ONNX Runtime 的图优化调度做的就是这件事——在执行之前,先把计算图“重新编排”一遍。
算子融合是最经典的优化。后厨里“焯水→爆炒”本是两道工序,中间要过一遍凉水、换一次锅,费时费力。融合之后,焯完水直接下锅炒,中间环节全省。ONNX Runtime 会把 Conv + BatchNorm + ReLU 这种高频组合融合成一个自定义算子,内核启动次数从三次降为一次,中间张量也无需写回内存。
拓扑排序决定先做哪道菜。图执行器按照依赖关系排出无环的执行顺序,保证每个算子的输入就绪后才被调度。子图划分则是把整个计算图切分成可在 CPU、GPU 等不同设备上执行的子图。内存复用优化让多个不再使用的中间张量共享同一块缓存,像洗碗工把盘子洗干净立刻给下一道菜用,省下大量内存分配开销。
图优化调度像是大厨在营业前把菜谱改得行云流水。一次好的图优化,效果比盲目加线程还明显——少了反复启动和搬运,后厨的自然就出菜快了。这也是为什么 ONNX Runtime 在执行前会先跑一轮优化 pass 的原因。
- T_{fused}(融合后的总耗时)合并后算子总执行时间,等于纯计算耗时加一次启动开销。
- t_{compute}(纯计算时间)真正做数值运算消耗的时间,算子融合不会减少这部分。
- t_{launch}(内核启动开销)每次调用执行内核的固定成本,类似换一口锅的时间。
- \sum_{i=1}^{n}\left(t_i + t_{launch}\right)(融合前总耗时)n 个算子各自算完、各自启动,总时间来自叠加。
线程配置策略:调整帮厨人数
既然有线程池,那该配多少人?ONNX Runtime 提供了两个旋钮:intra_op_num_threads 和 inter_op_num_threads。前者控制算子内并行的线程数(切墩组几人),后者控制算子间并行的线程数(炒锅组几口锅)。两个旋钮不是越大越好,需要根据模型结构和服务器核数精心调配。
在代码里只需两行:
线程绑定是另一个关键旋钮。默认情况下操作系统可以任意调度线程,这会导致 CPU 缓存频繁失效。把线程绑定到固定物理核,就像每个帮工固定用同一把菜刀——刀具趁手,切菜自然快。NUMA 感知更进一步:在多路服务器上,CPU 访问不同内存条的速度不一样。聪明的调度器会让线程优先使用本节点内存,减少跨总线的远程读取。
动态调整指的是运行时根据负载自动增减并行度。比如在线推理服务白天流量大,可以用多线程模式;夜间流量低,切换成低功耗单线程模式。
调整帮厨人数是一门平衡的艺术:人手太少出菜慢,人手太多挤在一个灶台反而碍手碍脚。用并行效率这个仪表盘,你可以一眼看出配置是否合理——效率跌破 50%,就该考虑减员或换优化策略了。
- E(并行效率)每增加一个线程实际换来的加速收益百分比。
- S_N(N线程下的加速比)用 N 个线程后相比单线程提升的倍数。
- N(线程数)实际使用的并行线程数量。
- \times 100\%(换算百分比)把效率从小数转化为直观的百分数,方便对比不同配置。
so = ort.SessionOptions()so.intra_op_num_threads = 4so.inter_op_num_threads = 2
性能分析:找到拖后腿的环节
后厨生意好不好,不能光看“出了多少菜”,还要看“客人等了多久”。ONNX Runtime 多线程推理的性能,同样需要从两个维度审视:吞吐量(每秒能处理多少请求)和延迟(单个请求从进到出花多久)。两者常常此消彼长——多线程榨出高吞吐,但线程间的协调也可能让单请求延迟微升。
性能分析最常用的指标是 CPU 利用率。如果开着 8 线程跑模型,CPU 利用率却只有 40%,说明线程在空等——可能是数据喂得太慢,也可能是线程竞争锁太厉害。线程竞争就像两个帮工抢同一把勺:一个紧紧攥着锁,另一个只能干瞪眼。上下文切换过多时,CPU 大量时间耗在切换线程上而不是真正的计算上。
瓶颈定位的逻辑是从整体到局部:先用 profiler 看每个算子的耗时占比,找出最耗时的前三个算子,再深入分析它们是否吃满了并行度。比如一个矩阵乘算子在 4 线程下只用了 1.2 倍加速,那就该检查是数据量太小拆不开,还是内存带宽到了极限。
在后厨,一道菜上慢了可能是切菜的磨洋工,也可能是等锅等太久。性能分析的意义正是找出那个“磨洋工”的环节。记得先测数据再下结论——优化靠直觉,很容易把时间浪费在根本无关紧要的步骤上。
- U(CPU 利用率)线程池中 CPU 实际执行计算的时间占比。
- T_{busy}(忙碌时间)线程真正执行算子计算的时间,不包含等待、锁竞争、切换开销。
- T_{total}(总时间)从任务入队到任务完成的全部时间。
- \times 100\%(转百分比)将忙碌占比变成直观百分比,低于 60% 通常需要排查线程等待问题。
工程实践:真实餐厅的经营之道
理论讲完了,回到真实世界的烟火气。假设你正在部署一个图像分类服务,模型是 ResNet-50 转成的 ONNX 格式。服务器的 CPU 是 16 核,你该用几个线程?经验公式是:intra_op = 4、inter_op = 2 起步,然后做网格搜索。图像模型里卷积算子是计算大头,调高 intra_op 能显著加速;如果模型结构是类似 GoogLeNet 这种多分支并行结构,增加 inter_op 效果更好。
NLP 场景就不太一样。BERT 这类 Transformer 模型以矩阵乘为主,算子间依赖强,多设 inter_op 线程意义不大,反而应该把所有线程都集中到 intra_op 上。更进阶的玩法是动态批处理:把并发到达的多个请求攒成一波,用数据并行一次性推理,吞吐量能翻好几倍——就像后厨把四个人的单子合并成一大锅炖。
实践中最大的坑是死锁与竞争排查。如果自定义算子里调用了一个会等待其他线程释放锁的函数,而那个线程又卡在这个算子的任务队列上——两个师傅互相等对方的锅,整个推理服务就冻住了。排查办法是先用 top -H 看线程状态,再用 gdb 抓线程栈,确认是锁等待还是死循环。
经营好一家真实的后厨,靠的不是某一招绝活,而是对每个环节的了解。图像分类靠 intra_op 提速度,NLP 靠动态批处理提吞吐,死锁排查靠线程栈定位。把这些经验沉淀成自己的调优清单,下一次遇到新模型就能快速找到最优配置。
- T_{e2e}(端到端延迟)一个推理请求从进入服务到返回结果的完整耗时,是用户真实感知的延迟。
- T_{input}(输入预处理)图像解码、缩放、归一化等前处理操作耗时,占整体比例常被低估。
- T_{infer}(模型推理)ONNX Runtime 执行计算图的核心耗时,多线程优化主要作用在这里。
- T_{output}(后处理)结果解码、排序、过滤等后处理耗时,NLP 场景下常成为隐藏瓶颈。
AI 应用场景
电商推荐系统在线推理
阶段:模型部署与在线推理
解决的问题:推荐模型每天承受数亿次调用,单线程推理延迟高、吞吐不足,高峰时段 GPU 成本过高。利用 ONNX Runtime 的多线程推理将模型跑在 CPU 集群上,按请求量动态调整线程数,把吞吐量提升 3-5 倍。
具体案例:某电商平台将召回模型从 TensorFlow 原生部署迁移到 ONNX Runtime,设置 intra_op_num_threads=8,配合动态批处理,将 p99 延迟从 45ms 压到 28ms,同时释放了 GPU 资源给排序模型使用。
医疗影像批量诊断
阶段:离线批量推理
解决的问题:医院每天产生上千张 CT 影像,需要逐一进行病灶分割。单线程逐张推理要跑数小时。利用 IntraOp 数据并行,将多张影像切分到多个线程同时处理,让批量推理时间缩短 70%。
具体案例:某医学影像团队用 ONNX Runtime 部署肺结节分割模型,16 核 CPU 上设置 12 个 intra 线程,配合图优化中的算子融合,处理 1000 张影像从 6 小时压缩到 1.8 小时。
智能安防视频流实时检测
阶段:流式推理与流水线并行
解决的问题:视频流需要逐帧检测,单帧推理时间过长会导致丢帧或延迟累积。利用流水线并行让视频解码、模型推理、结果上报三阶段同时进行,类似后厨的传菜流水线。
具体案例:某安防厂商在 8 路 RTSP 视频流上部署 YOLOv5 检测,通过 ORT 流水线并行让每路视频保持 25 FPS 实时处理,CPU 利用率稳定在 85% 以上。
Python 演示
# -*- coding: utf-8 -*-"""ONNX Runtime 多线程推理性能对比演示"""import timeimport numpy as npimport onnximport onnxruntime as ortfrom onnx import helper, TensorProto def build_demo_model(path="demo_model.onnx"): """构造一个简单的三层 MLP 模型并保存为 ONNX 文件""" X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 64]) Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 10]) weight1 = np.random.randn(64, 128).astype(np.float32) weight2 = np.random.randn(128, 10).astype(np.float32) initializers = [ helper.make_tensor("W1", TensorProto.FLOAT, [64, 128], weight1.flatten()), helper.make_tensor("B1", TensorProto.FLOAT, [128], np.zeros(128, dtype=np.float32)), helper.make_tensor("W2", TensorProto.FLOAT, [128, 10], weight2.flatten()), helper.make_tensor("B2", TensorProto.FLOAT, [10], np.zeros(10, dtype=np.float32)), ] nodes = [ helper.make_node("MatMul", ["X", "W1"], ["fc1"]), helper.make_node("Add", ["fc1", "B1"], ["fc1b"]), helper.make_node("Relu", ["fc1b"], ["act1"]), helper.make_node("MatMul", ["act1", "W2"], ["fc2"]), helper.make_node("Add", ["fc2", "B2"], ["Y"]), ] graph = helper.make_graph(nodes, "demo_mlp", [X], [Y], initializers) model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) onnx.save(model, path) return path def benchmark(model_path, intra, inter, rounds=100): """在指定线程数配置下测量平均推理延迟(毫秒)""" so = ort.SessionOptions() so.intra_op_num_threads = intra so.inter_op_num_threads = inter sess = ort.InferenceSession(model_path, so, providers=["CPUExecutionProvider"]) x = np.random.randn(1, 64).astype(np.float32) for _ in range(10): # warmup sess.run(None, {"X": x}) start = time.perf_counter() for _ in range(rounds): sess.run(None, {"X": x}) avg_ms = (time.perf_counter() - start) / rounds * 1000 return avg_ms if __name__ == "__main__": model_path = build_demo_model() print("=== ONNX Runtime 多线程推理性能对比 ===") for intra, inter in [(1, 1), (2, 1), (4, 1), (4, 2), (8, 2)]: avg = benchmark(model_path, intra, inter) print(f"intra={intra:2d} inter={inter:2d} | 平均延迟 {avg:8.2f} ms")
sess_options = ort.SessionOptions()创建会话配置对象,所有线程旋钮都挂在这个“控制面板”上。so.intra_op_num_threads = intra设置算子内并行线程数,决定单个算子拆成几块算。so.inter_op_num_threads = inter设置算子间并行线程数,决定同时跑几个独立算子。sess.run(None, {"X": x})真正执行一次推理,第一个参数为 None 表示返回所有输出。time.perf_counter()高精度计时器,比time.time()更适合毫秒级性能测量。
常见误区
误区我机器有 32 核,那把 `intra_op_num_threads` 设成 32,推理速度肯定比设 8 快几倍吧?
正解线程数是“越多越快”最典型的错觉。线程过多会带来三方面开销:一是线程切换消耗 CPU 周期;二是多线程争抢内存带宽,反而互相拖慢;三是同步等待成本上升。实测中,许多模型在 8 线程后性能就进入平台期,甚至下降。最优线程数需要结合模型计算量、数据大小和 CPU 架构实测得出,而不是盲目堆人数——后厨塞下 50 个帮工反而谁都迈不开腿。
为何常见:直觉上“人多力量大”,叠加厂商宣传多核性能,让人误以为并行度与性能是线性关系,忽略了 Amdahl 定律和系统瓶颈。
误区既然有两个线程池,那就各分一半核数,比如 16 核就 IntraOp=8、InterOp=8,这样最平衡。
正解两个线程池的分工完全不同,正确比例取决于模型结构。卷积层多的模型计算密集,应把大部分线程给 IntraOp;图分支多的模型并行支路多,才值得给 InterOp 多配线程。且 InterOp 线程数超过可并行分支数时,多出的线程完全是闲置资源。正确做法是从 1:1 起步,逐个调整做网格搜索,找到当前模型的最优比例。
为何常见:人们追求“平均完美”的心理在配置场景中很普遍,但忽略了两个池子面对的任务量天然不对等。
误区上线了多线程推理后,单个请求的耗时就该从 30ms 降到 10ms。
正解多线程推理的核心收益往往是吞吐量,而非单请求延迟。算子内并行可能让单次计算略快,但线程间同步、任务分发、内存竞争都可能给单请求增加额外开销。尤其在请求本身计算量很小时,多线程的协调成本甚至可能让延迟变差。生产环境中常见策略是“保吞吐、稳延迟”:用多线程把 QPS 撑上去,用批处理和队列控制单请求延迟。如果只为降低单次延迟,有时单线程反而表现更好。
为何常见:大家天然把“并行”等同于“更快”,但忽略了并行本身也有成本,延迟和吞吐是两个不同维度的指标。
误区线程数设置对了就万事大吉,操作系统会负责把线程调度到合适的核上。
正解多路服务器上,放任系统调度会让线程在不同 NUMA 节点间漂移,导致每次访问内存都要跨越总线,延迟增加 30% 到 100%。合理做法是绑定线程到固定物理核(CPU 亲和性),并让线程尽量使用本节点的内存——就像帮工固定用自己的灶台和菜刀,换台换刀都会增加额外时间。这在虚拟化环境中尤其重要:宿主机上的 CPU 竞争会让推理抖动加剧。
为何常见:操作系统调度对上层透明,人们习惯性地信任“黑盒”,只有深入理解硬件拓扑后才会意识到调度漂移的巨大代价。
误区图优化能做的很有限,与其调这些虚的,不如加 CPU 或者换更贵的 GPU。
正解图优化带来的是“结构性优势”,与硬件升级不冲突但常比硬件升级更划算。算子融合能减少内核启动次数和中间内存搬运,这往往是 CPU 推理的最大开销来源。实测中,Conv+BN+ReLU 融合能让对应部分提速 2 到 3 倍。图优化可以提前做,而硬件升级要花真金白银。更关键的是,图优化后的模型占用的内存更小、缓存命中率更高,这些收益是堆硬件换不来的。
为何常见:图优化过程不可见,“看不到所以不重要”;而硬件是看得见摸得着的升级,这种心理偏差让很多团队忽略了性价比最高的优化手段。
面试题
Q1. ONNX Runtime 中负责“算子内部并行”的线程池是?
- A.IntraOp 线程池← 正确答案
- B.InterOp 线程池
- C.内存管理线程池
- D.主调度线程池
答案:A。IntraOp 指算子内的并行切分,比如矩阵乘法拆成多块交给多个线程算,就像切墩师傅把白菜分给几个人同时切。InterOp 负责算子间的并行,两者分工不同。
Q2. 线程池相比每次动态创建线程,最大的优势是什么?
- A.避免频繁创建销毁开销← 正确答案
- B.可以无限增加线程数量
- C.不需要任务队列
- D.保证算出的结果更精确
答案:A。动态创建线程涉及系统调用、内核对象分配和销毁,开销巨大。线程池预先创建好一批线程反复使用,像后厨养着一支固定帮工团队,来单就干、没单待命,省去频繁招聘和解雇的成本。
Q3. Amdahl 定律中的 p 代表什么?
- A.可并行部分的占比← 正确答案
- B.串行部分的占比
- C.线程池的总数量
- D.CPU 核心频率大小
答案:A。p 表示整个任务中可以被并行执行的占比。即使 p = 0.95(95% 可并行),理论上限也只有 20 倍加速。这解释了为什么多线程推理不能无限扩展,串行部分就像后厨里必须单锅炖的那道汤,谁来了都得等。
Q4. 设置 intra_op_num_threads=4 意味着什么?
- A.单个算子最多拆成 4 块并行计算← 正确答案
- B.一次最多同时运行 4 个算子
- C.模型被切成 4 段依次执行
- D.使用 4 个 CPU 做负载均衡
答案:A。IntraOp 线程池控制的是单个算子内部的并行度。比如一个卷积算子可以把输入通道分成 4 份,交给 4 个线程同时算,然后把结果拼起来。它对应后厨里“一份切墩活多人同时上手”的协作方式。
Q5. 算子融合最核心的性能收益来自哪里?
- A.减少内核启动与中间内存读写← 正确答案
- B.减少了模型的参数量
- C.增加并行线程的数量
- D.让模型支持更多输入格式
答案:A。算子融合把 Conv、BN、ReLU 等合并成一个内核,启动了从三次变成一次,中间张量不用写回主存再读出来。这个收益来自“省掉中间环节”,就像把焯水和爆炒合并成一道工序,中间的换锅、过水全免了。
Q6. 哪种模型结构最适合发挥 InterOp 线程池的优势?
- A.多分支并行结构的模型← 正确答案
- B.单层大矩阵乘法模型
- C.串行线性链式结构模型
- D.只有输入输出的空模型
答案:A。InterOp 依赖图中存在互不依赖的并行分支,多个线程可以同时执行不同算子。像 Inception 这类多分支模型,可以让每个分支在不同线程上同时算,类似多个师傅各炒各的菜。串行模型没有并行支路,再多的 InterOp 线程也只能排队。
Q7. NUMA 感知线程配置的核心目的是什么?
- A.减少跨 NUMA 节点的远程内存访问← 正确答案
- B.增加线程池的最大线程数
- C.减少网络传输的带宽消耗
- D.提高 GPU 显存的分配速度
答案:A。多路服务器中,CPU 访问本地内存远快于跨总线访问远端内存。NUMA 感知调度让线程优先使用所在节点的内存,减少远程访问延迟。这类似让每个帮工用离自己不远的菜篮——跨越大半个厨房拿食材,累且慢。
Q8. 下列哪些问题是多线程推理中常见的安全隐患?
- A.死锁导致推理服务冻结← 正确答案
- B.线程竞争锁导致性能骤降← 正确答案
- C.上下文切换开销浪费 CPU← 正确答案
- D.模型推理精度高于预期
答案:ABC。死锁让所有线程互相等待,服务完全卡死;线程竞争锁让大量时间耗在等待而非计算;上下文切换过多则让 CPU 周期浪费在换人上。这些都属于并发编程的系统性成本。模型精度提升显然不是问题,而是目标。
Q9. 下列哪些做法有助于提高多线程推理的实际吞吐量?
- A.开启动态批处理合并请求← 正确答案
- B.对图执行算子融合优化← 正确答案
- C.无脑调大线程数到 64
- D.设置 CPU 亲和性绑定核← 正确答案
答案:ABD。动态批处理用数据并行提升吞吐;算子融合减少启动和内存搬运;CPU 亲和性避免线程漂移带来的缓存失效。而无脑调大线程数会引起上下文切换和带宽争抢,通常适得其反。后厨也不是帮工越多越好——灶台不够,人多反而绊脚。
Q10. Little 定律 L = λ × W 中,当推理系统达到稳态时,W 代表什么?
- A.请求在系统中平均停留时间← 正确答案
- B.每秒到达的新请求数量
- C.系统中排队的请求总数
- D.工作线程池的总线程数
答案:A。Little 定律是排队论的基石:队列中的平均请求数 L 等于到达率 λ 乘以平均停留时间 W。对推理服务而言,W 就是请求从进来到返回的平均耗时。这好比后厨里站着的等待订单数 = 每分钟新订单数 × 每单平均等待时长。想缩短排队,要么提高出菜速度(降 W),要么限流(降 λ)。