C++ / a working model

114 / 163   ·   C11   ·   约 8 分钟

多处理器调度(进阶)

先记住这句话

随着多核芯片普及,操作系统必须在多个CPU间分配线程。本章用原创视角讲解缓存层次带来的一致性难题、共享数据仍需锁保护的原因,以及调度器如何利用缓存亲和性来减少迁移开销。

本篇内容
  1. 多核普及带来的新调度任务
  2. 每核缓存如何破坏简单内存视图
  3. 锁与亲和性:调度器必须同时考虑的两件事
  4. 运行示例
  5. 动手练习

官方章节 PDF

多核普及带来的新调度任务

芯片设计师无法再靠提高单核频率来获得性能,因为功耗会急剧上升,于是把多个较慢的核心放进同一颗芯片。普通顺序程序只能占用其中一个核心,因此程序员必须把工作拆成线程才能真正用上额外核心。操作系统此时不再只决定“下一个跑谁”,还要决定“跑在哪颗核心上”,这带来了负载均衡与数据局部性之间的新权衡。

每核缓存如何破坏简单内存视图

每个核心都拥有自己的小而快的缓存,用来保存最近访问过的数据和指令。当一个核心改写了某个地址后只更新自己的缓存、尚未写回主存时,另一个核心如果立刻去读同一地址,就会从主存拿到过期值。硬件通过总线侦听等协议让各缓存互相通知失效或更新,从而维持表面上的共享内存。即便如此,软件仍不能假设一次普通读写是原子的。

锁与亲和性:调度器必须同时考虑的两件事

即使硬件保证了最终一致性,多个核心同时修改同一链表或队列仍会导致丢失更新或重复释放。因此共享结构必须用互斥锁保护。锁本身会成为可扩展瓶颈。与此同时,线程一旦在某核上运行过,该核缓存里就装满了它的热数据;把它迁到另一核会引发大量缓存缺失。优秀的多处理器调度器因此尽量让线程留在原来的核心(缓存亲和性),只在负载严重不均时才迁移。

常见误区

  • 以为多加几个核就能让任意单线程程序自动变快
  • 认为硬件缓存一致性已经足够,共享队列不必再加锁
  • 为了绝对公平而频繁迁移线程,导致缓存不断被冲刷

运行一个例子

最低标准 C11 · 完整程序 · 下载 .c

#include <stdio.h>

int main(void) {
    printf("Multiprocessor Scheduling Simulation\n");
    printf("====================================\n");
    printf("Assigning jobs considering cache affinity:\n");
    printf("CPU 0: JobA (initial placement)\n");
    printf("CPU 1: JobB (initial placement)\n");
    printf("CPU 0: JobC (affinity to previous cache)\n");
    printf("CPU 1: JobD (load balance)\n");
    printf("Note: Keeping jobs on same CPU preserves cache contents.\n");
    return 0;
}

在本地编译

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-10-multiprocessor.c -o example && ./example

预期结果

Multiprocessor Scheduling Simulation
====================================
Assigning jobs considering cache affinity:
CPU 0: JobA (initial placement)
CPU 1: JobB (initial placement)
CPU 0: JobC (affinity to previous cache)
CPU 1: JobD (load balance)
Note: Keeping jobs on same CPU preserves cache contents.

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

某个线程在CPU 0上修改了变量x后被调度器立刻迁到CPU 1,CPU 1上的第一次读可能看到什么?为什么?

查看参考答案

可能读到修改前的旧值。因为写可能还停留在CPU 0的缓存里,主存尚未更新,而CPU 1的缓存是空的,只能从主存取数;若硬件侦听尚未完成失效,就会得到过期数据。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录