线程迁移具体是如何做到的?

41 阅读3分钟

总答案

线程迁移并不是“把正在跑的线程拽走”,
而是:在线程“没在跑”的瞬间,
把它从一个 core 的可运行队列,挪到另一个 core 的可运行队列。

⚠️ 关键点
👉 迁移几乎总是发生在“线程不在 Running 状态”时


一、调度器真正能“操作”的是什么?

调度器不能直接操作 Core,也不能强行移动正在执行的指令流

唯一能直接操作的对象是

**线程的调度元数据(TCB / task_struct)

  • 线程所在的 runnable 队列**

所以迁移的本质是:

修改“这个线程下一次会被哪个 core 选中”的规则


二、Linux 调度里的基本结构

在 Linux(以 CFS 为例)中,逻辑上是这样:

  • 每个 CPU core:

    • 有一个 本地 run queue
  • 每个 runnable 线程:

    • 只会在 某一个 run queue
  • 一个线程在哪个队列里:

    • 决定了它只能被哪个 core 调度执行

所以:

迁移 = 把线程从一个 run queue 挪到另一个 run queue


三、什么时候“会考虑迁移”?

调度器并不是每一刻都在做迁移,它通常在几个明确的时机点考虑这件事。

常见触发点(重要)

1️⃣ 周期性负载均衡(Periodic Load Balancing)

  • 内核有定时机制

  • 定期检查:

    • 哪些 core 很忙

    • 哪些 core 很闲

如果发现负载不均:

  • 就会尝试把一些 runnable 线程迁移出去

2️⃣ 线程刚变成 runnable 的时候

比如:

  • 一个线程:

    • 刚从 IO 返回

    • 刚被 condvar 唤醒

    • 刚释放锁、从 blocked → runnable

这时:

调度器会重新决定:
你是回原来的 core,
还是放到更空闲的 core?


3️⃣ 新线程创建时

新线程(fork / clone / pthread_create):

  • 一开始并不“属于”某个 core

  • 调度器会选择一个当前负载较轻的 core

  • 直接把它放进那个 core 的 run queue


四、迁移“具体是怎么做的”(一步一步)

走一遍迁移流程

场景设定

  • Core 0:很忙(run queue 很长)

  • Core 3:很闲(run queue 很短)

  • 线程 T:当前是 runnable,但没有在 Running


Step 1:调度器发现“不平衡”

调度器检测到:

load(core0) >> load(core3)

于是决定:
👉 “应该把一些线程从 core0 挪到 core3”


Step 2:选择“可迁移的线程”

⚠️ 不是所有线程都能迁移

调度器会排除:

  • 正在 Running 的线程

  • 被设置了严格 CPU affinity 的线程

  • 某些调度策略下不宜迁移的线程

剩下的候选线程中:

  • 通常选择:

    • 最近没怎么跑的

    • 或者负载贡献较小的

    • 或者“迁移成本较低”的


Step 3:从原 run queue 移除

调度器会:

  • 给 core0 的 run queue 加锁

  • 把线程 T 从 core0 的 runnable 队列中摘下来

  • 更新 T 的调度元数据:

    • “我不再属于 core0 了”

Step 4:加入目标 run queue

然后:

  • 给 core3 的 run queue 加锁

  • 把线程 T 插入 core3 的 runnable 队列

  • 更新 T 的“目标 CPU”字段

此时:

线程 T 的“归属 core”已经变了

但注意:

  • 它还没跑

  • 只是“排队位置变了”


Step 5:等待被调度执行

接下来发生的事情是自然的:

  • core3 结束当前线程 / 变空闲

  • 从自己的 run queue 里选一个线程

  • 发现线程 T

  • 执行 T

👉 这时你才“观察到”:
线程 T 跑到了 core3


五、非常关键的一点(很多人误解)

❌ 迁移 ≠ 抢走正在执行的线程

调度器不会

  • 把正在 core0 上跑的线程硬拽走

  • 在执行中途“扔到另一个 core”

✅ 迁移发生在“安全点”

  • 线程未运行

  • 或刚结束一次运行

  • 或刚被唤醒、尚未执行

这样才能:

  • 保证执行语义正确

  • 避免破坏寄存器 / pipeline 状态


六、那 cache 是什么时候“冷”的?

  • 线程 T 在 core0 跑时:

    • cache line 在 core0 的 L1/L2
  • 迁移后:

    • core3 的 L1/L2 没有这些数据
  • 第一次在 core3 跑时:

    • cache miss 爆发

    • 工作集重新加载

这就是在性能分析里看到的:

迁移 → cache 冷启动 → 延迟抖动