多线程

原理

内核线程模型

std::thread 创建的是真正的内核级线程(KLT)。在 Linux 上底层调用 pthread_createclone() 系统调用。每个线程由操作系统内核管理,与进程共用调度器的调度队列。

CPU 核心与超线程

一台”8核16线程”的 CPU 中只有 8 个物理核心——每个物理核心内有两套寄存器组共享一套 ALU/FPU。当前线程因缓存未命中等待内存时,CPU 可切换到另一线程。对于计算密集型任务,超过物理核心数的线程不会有性能提升(反而因上下文切换降低性能)。

CFS 完全公平调度器(Linux)

Linux 内核的 CFS 调度器维护每个线程的虚拟运行时间(vruntime),使用红黑树管理线程,选择 vruntime 最小的线程运行。O(log n) 复杂度的选择。在一个调度周期内,所有可运行线程按其权重(nice 值决定)比例获得 CPU 份额。

线程间共享与独立资源

共享:堆、全局/静态变量、代码段、文件描述符表、进程 ID。

独立:每个线程有自己的栈(Linux 默认 8MB)、寄存器组、线程 ID、errno(TLS 实现)、信号掩码。

数据竞争的本质

C++ 标准定义:两个表达式求值,若其中一个修改内存位置、另一个读取或也修改同一内存位置,且这些求值之间没有 happens-before 关系——则构成数据竞争。数据竞争导致未定义行为

经典例子:两个线程同时执行 counter++,由于 ++ 不是原子操作(读-改-写三步),可能导致两个线程读到相同的旧值。

检测手段:g++ -fsanitize=thread(Thread Sanitizer),valgrind --tool=helgrind

MESI 缓存一致性协议

现代 CPU 每个核心有自己的 L1/L2 缓存。MESI 协议确保缓存一致性,每个缓存行(64 字节)处于四种状态之一:

  • M (Modified):本核心独有,已修改,与内存不一致
  • E (Exclusive):本核心独有,与内存一致
  • S (Shared):多个核心共享,与内存一致
  • I (Invalid):无效,读取需从其他核心或内存获取

内存屏障与 memory_order

即使有 MESI 协议,CPU 和编译器仍可能对指令进行重排序(为提高性能)。std::atomicmemory_order 控制重排序边界:

内存序保证开销
relaxed仅原子性最低
acquire后续操作不重排到此 load 之前
release之前的操作不重排到此 store 之后
seq_cst(默认)全局统一顺序最高

acquire/release 配对是实现无锁编程的经典模式:写线程用 release 发布数据 → 读线程用 acquire 消费数据。

虚假唤醒(Spurious Wakeup)

条件变量的 wait() 即使没收到 notify 也可能被唤醒——这是操作系统和硬件实现层面的细节。因此必须用带谓词的 wait

cv.wait(lock, []{ return !queue.empty(); });  // 正确
// cv.wait(lock);  // 错误——虚假唤醒时条件可能不满足

语法

std::thread

std::thread t(func, arg1, arg2);     // 创建线程
t.join();                             // 等待完成
t.detach();                           // 分离(线程在后台运行)
t.joinable();                         // 是否可 join
std::thread::hardware_concurrency();  // 逻辑核心数

必须在 std::thread 对象析构前调用 join()detach()——否则 std::terminate()

std::mutex 与 RAII 锁

std::mutex mtx;
{
    std::lock_guard<std::mutex> lock(mtx);   // RAII:构造 lock,析构 unlock
    // 临界区
}
 
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定
lock.lock();  // 手动锁
// unique_lock 支持 move,lock_guard 不支持

std::scoped_lock(C++17)——死锁避免

std::scoped_lock lock(mtx_a, mtx_b);  // 原子性地锁定多个 mutex
// 等价于 std::lock(mtx_a, mtx_b) + adopt_lock

std::condition_variable

std::mutex mtx;
std::condition_variable cv;
std::queue<T> q;
 
// 生产者
{ std::lock_guard lk(mtx); q.push(x); }
cv.notify_one();
 
// 消费者
std::unique_lock lk(mtx);
cv.wait(lk, []{ return !q.empty(); });  // 带谓词的 wait
T x = q.front(); q.pop();

std::atomic

std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed); // 原子加法
counter.store(42);                                 // 原子写入
int v = counter.load();                            // 原子读取
// compare_exchange_strong/weak — CAS 原子比较交换

std::call_once(线程安全的单次初始化)

std::once_flag flag;
std::call_once(flag, []{ init(); });  // 多线程环境中只执行一次

std::future / std::async

auto fut = std::async(std::launch::async, compute, arg1, arg2);
int result = fut.get();  // 阻塞直到结果就绪

thread_local

thread_local int thread_specific_counter = 0;
// 每个线程拥有自己独立的副本

实践

力扣题目:无专属多线程练习题。建议实践:1) 用多线程并行计算 π 值(蒙特卡罗方法),用 std::atomic 记录总采样数;2) 实现一个简单的线程池(使用 std::queue + std::condition_variable + std::mutex)。

AI 自检:要求 AI 解释以下代码的数据竞争问题并提出三种解决方案:

int counter = 0;
void worker() { for (int i=0; i<10000; ++i) ++counter; }
int main() {
    std::thread t1(worker), t2(worker);
    t1.join(); t2.join();
    std::cout << counter; // 不确定
}

建议先阅读04_动态内存 — 堆内存的线程安全问题;12_信号处理 — 多线程环境中信号的行为。