多线程
原理
内核线程模型
std::thread 创建的是真正的内核级线程(KLT)。在 Linux 上底层调用 pthread_create→clone() 系统调用。每个线程由操作系统内核管理,与进程共用调度器的调度队列。
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::atomic 的 memory_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_lockstd::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; // 不确定
}