16线程为何占用16C?Linux多核调度与负载均衡解析

解析线程并行上限、CFS运行队列及多核负载均衡的关系。

冷月清谈:

文章以40核服务器上的16线程CPU密集型进程为例,区分了CPU消耗总量与各核心利用率:线程数量决定理论并行上限,16个持续可运行线程最多消耗约16C;监控中的单核利用率则是采样窗口内忙碌时间的平均值。文中进一步说明,Linux实际以线程对应的task为调度单位,每颗CPU拥有独立运行队列,CFS通过唤醒均衡、周期均衡和空闲均衡在调度域内迁移任务。文章还列出了结论成立的前提,包括未设置CPU亲和性、cpuset或cgroup配额,且没有实时任务及显著外部负载。NUMA、超线程、容器限额和混合负载都会改变观测结果。需要注意,CFS会兼顾缓存局部性和迁移成本,实际环境不一定始终形成绝对均匀的每核占用。

怜星夜思:

1、真实环境中,16个纯计算线程真的会在40颗CPU上平均成每核40%吗?
2、怎样判断进程的16C上限来自线程数量,还是来自cgroup或容器CPU配额?
3、把线程固定到指定CPU,性能一定会比让CFS自动调度更好吗?
4、40个逻辑CPU是否等价于40颗物理核心的计算能力?

原文标题:Linux下并发线程与CPU调度机制

原文作者:牧羊人的方向

原文内容

一台40核Linux服务器上运行着一个16并发线程的进程A。监控数据显示进程A的CPU占用上限为16C(约1600%),与线程数吻合;但系统级观测显示,CPU0至CPU39每核利用率均在40%左右,没有任何一颗核被打满,也没有任何一颗完全空闲。从进程视角看,其CPU占用已触及16C的并发上限;从系统视角看,40颗核均处于半负载状态。两个观测结果单独看均正确,合并分析则呈现出非线性关系:16个线程为何会均匀分布于40颗核之上?

这其中涉及到Linux 线程调度与CPU负载均衡机制,本文分层次展开分析。

1、并发上限和CPU均衡分摊

进程的并发上限锁定了CPU消耗总量,而负载均衡决定了算力的空间分布。进程CPU使用总量为16C,分摊至40颗CPU核,每颗核平均即为40%。

  • "最多16C"由进程自身的可运行线程数决定。16个线程同时最多占用16颗CPU,这是并行度上限,与系统CPU核总数无关。即使系统拥有128核,该进程仍最多使用16核的算力。
  • "每核约40%"由调度器的负载均衡机制决定。内核默认的CFS调度器会将线程分摊至所有核,因此16份算力被40颗核平均瓜分,每核分得16/40≈40%。

以下给出最小复现示例,便于验证(16个CPU密集线程+40核服务器):

// 示意:16个CPU密集线程
pthread_t t[16];
for (int i = 0; i < 16; i++)
    pthread_create(&t[i], NULL, busy_loop, NULL); // while(1){}

观测命令

mpstat -P ALL 1        # 每核使用率 → 全部 ≈ 40%
top -H -p <pid>        # 每个线程 CPU → 16 个线程各约 100%
ps -eLf | grep <pid>   # 确认线程数 = 16

下文从"调度的最小单位"开始,逐层分析至负载均衡机制。

2、调度的最小单位线程

“进程"常被直观理解为调度对象,但是在Linux中能够被调度器安排至CPU中执行的最小单位是线程(内核中称为task)而非进程。进程仅是"一组线程的集合”(内核中称为线程组 thread group),其自身不占用CPU、也不参与调度排队。进程的CPU占用为其名下所有线程占用的总和。16并发线程对应16个可运行task,因此进程A的总CPU消耗上限为16颗核的满负荷,即16C。

底层实现上,线程通过clone()系统调用创建,携带CLONE_THREAD等标志,与同组线程共享地址空间mm、文件描述符表files、信号处理signal等资源;但每个线程拥有独立的task_struct(任务描述符),内含独立的调度信息,如调度策略、优先级/nice值、以及CFS用于记账的vruntime。调度器视角下不存在进程A,而是16个独立的调度实体。

单个线程在任一时刻仅能运行于一颗CPU,16个线程即构成16个并行位,系统CPU核总数再多也无法增加并行度。但为何不是16颗CPU核打满24颗核空闲,而是40颗核均处于40%?这需要深入调度器内部机制。

3、per-CPU运行队列机制

Linux调度器(CFS,完全公平调度器)采用每颗CPU一个运行队列的设计(runqueue,内核中称为 rq),而非全局队列。

  • 队列结构:每个rq内含一个cfs_rq,使用红黑树按虚拟运行时间vruntime组织所有可运行线程。vruntime越小,表明该线程累积的CPU欠账越多,优先级越高。
  • 选择线程:调度时调用pick_next_task_fair(),直接选取红黑树最左节点(最小vruntime)上核运行。
  • 记账与抢占:每次时钟中断tick,内核执行update_curr()累加当前线程的vruntime,再通过check_preempt_tick()判定是否触发抢占(上下文切换switch_to())。
  • 结构约束:单个线程在任一时刻仅可挂载于一颗CPU的rq上排队或运行,不可能同时存在于两颗CPU核。

按照16个线程 × 每线程同时仅占一颗CPU = 16个并行位。那么16份算力会均匀分布于全部40颗CPU核,而非集中于16颗CPU核?答案在于内核的负载均衡器。

4、CPU负载均衡机制

若调度器将16个线程固定绑定于16颗CPU,观测结果应为"16颗CPU核100%、24颗核0%"。但默认CFS的设计目标恰好相反,CFS使负载在CPU核间尽可能均匀,因此线程会在 40颗核之间持续迁移。内核主要包含三条均衡路径:

  • 唤醒均衡(wake-up balancing):线程被唤醒或新建时,select_task_rq_fair()结合wake_affine()选择目标核,优先选择空闲核、或与唤醒者缓存关系更近的核。
  • 周期均衡(periodic balancing):调度器按调度域(SMT → 同簇 → NUMA 节点 → 全系统)周期性执行 run_rebalance_domains(),通过find_busiest_group()/find_busiest_queue()定位最繁忙的队列,将线程迁移至较空闲队列。
  • 空闲均衡(idle balancing):CPU核即将空闲时执行 idle_balance()(含nohz下的空闲均衡),主动从其他队列拉取线程;若无法拉取,则通过active balance借助stopper线程强制迁移。

16个CPU密集线程(持续运行,无睡眠、无IO等待)保持可运行状态,负载均衡器将其运行机会轮流分配至40颗核。因此在任何采样窗口内,每颗核均在忙碌与空闲之间被均匀分摊。不过真实内核中的负载均衡还受调度域层级(SMT/同簇/NUMA节点)、迁移代价、超线程共享等约束影响,并非理想的绝对平均。

Linux中通过sched_domain 的层次结构做负载均衡,粒度从细到粗:

SMT 域          (同一物理核内的2个逻辑CPU)
  └→ MC 域     (同一LLC内的核)
      └→ DIE 域 (同一芯片内的多个LLC)
          └→ NUMA 域 (同一NUMA节点)
              └→ GLOBAL域 (跨节点)

每层域有自己的 load_balance() 触发条件和迁移成本:

域
触发频率
迁移代价
典型行为
SMT
每次tick
极低
把任务从忙的兄弟核拉到闲的兄弟核
MC
定期(sd->balance_interval)
低
迁移到同LLC的空闲核
NUMA
低频
高(跨内存控制器)
迁移到同节点的空闲核
GLOBAL
极低频
极高
尽量避免
5、总结

假设16个线程均为CPU密集型(持续运行,无sleep、无IO等待),则每秒总需求16个线程 × 每线程满负荷100% = 16核·秒/秒。此即进程A的CPU占用上限16C(top显示约1600%)的物理含义,每秒消耗相当于16颗核满负荷运行一整秒的CPU时间。而系统具有40颗核,每秒提供40核·秒的算力。负载均衡器将需求分摊至所有核,每颗核每秒被占用16 ÷ 40 = 0.4 核·秒,即利用率40%。

该40%为时间平均口径:mpstat、top等工具按固定周期采样/proc/stat,以窗口内忙碌时间除以墙钟时间计算。16个线程轮流在CPU核间迁移,每个采样窗口内每核约有40%的时间执行该进程的线程,因此既不会出现16核打满,也不会出现任何一颗核完全空闲。

本文推导成立的前提为:CFS(SCHED_OTHER)默认配置、线程无CPU亲和绑定、无cpuset约束、无实时线程抢占、无cgroup配额、线程CPU密集且持续可运行、系统无其它显著负载。以下情况需调整结论:

  • 存在实时线程:实时线程优先级更高,会挤占CFS线程,分布形态将改变。
  • 容器/cgroup配额:配额将进程限制于16C时,出现的为throttle,而非"每核40%"。
  • NUMA绑定或cpuset:线程仅能在部分节点/核运行,负载均衡的全系统摊平失效。
  • 超线程:40逻辑核可能对应20物理核 × 2 线程,SMT兄弟核共享执行单元,利用率口径需按物理核重新计算。
  • 混合负载:系统存在其它进程、内核线程、中断时,每核使用率为全体负载之和,不能简单归因于进程A。

针对“绑核是否更快”:没有必然结论。绑核能减少迁移和缓存失效,对低延迟、缓存敏感任务有利;但绑定方案不合理会造成负载倾斜,还可能让线程跨NUMA节点访问内存,反而更慢。

2 个赞

最好直接做三组benchmark:默认调度、taskset绑核、numactl同时绑定CPU和内存。除了吞吐量,还要看P99延迟、上下文切换、CPU迁移次数以及远端NUMA访问,单看平均CPU占用不够。

3 个赞

不一定,调度器不是洒水车,不会为了让监控图好看就把线程挨个喷到40个核上。缓存命中、NUMA距离和当时有没有别的任务,都会影响它搬不搬家。

1 个赞

回答“线程上限还是配额”:先查看cgroup v2的cpu.max和cpu.stat。若nr_throttled、throttled_usec持续增长,通常是配额触发限流;若没有节流,而16个计算线程长期各占约100%,更像是线程并行度本身的限制。

2 个赞