多线程编程基础
基础概念 多线程编程之前,先搞清楚几个基本概念:并发、并行、进程、线程、协程。它们经常被混用,但含义差别很大,而且这些差异最终都落在硬件层面。 并发与并行 并发(concurrency)指多个任务在同一时间段内交替推进——宏观上看它们"同时"在进行,微观上可能只有一个执行单元在轮流处理。并行(parallelism)指多个任务在同一时刻真正同时执行,这要求有多个执行单元(多个 CPU 核心)。 两者的关系可以用一个经典表述概括:并发是程序结构层面的性质,并行是执行层面的性质。一个程序可以设计成并发的(多个任务交替推进),但只有在多核机器上运行时才可能并行。 单核并发(时间片轮流): CPU 核心: [任务A][任务B][任务A][任务B][任务A][任务B] ... └── 每个时刻只有 1 个任务在跑 ──┘ 双核并行: 核心 1: [任务A][任务A][任务A][任务A] ... 核心 2: [任务B][任务B][任务B][任务B] ... └── 同一时刻有 2 个任务在跑 ──┘ 注意:并发程序...
线程传参
需要预先学习: 多线程编程基础 传参的本质:构造时拷贝 std::thread 的构造函数把实参按值拷贝(或移动)到线程内部,然后在线程入口函数中使用这些拷贝。这一点决定了传参的大部分规则: #include <iostream> #include <thread> void print(int n) { std::cout << n << std::endl; } int main() { int a = 100; std::thread t(print, a); // a 被拷贝一份传给线程, 线程内部用的是副本 a = 200; // 修改主线程的 a, 不影响线程内的副本 t.join(); return 0; } 输出 100 而不是 200——线程拿到的是构造时的副本。参数在 std::thread 构造的那一刻就被拷贝/移动了,之后主线程再怎么改原变量,线程内部都不受影响。 注:拷贝发...
数据共享问题与锁
需要预先学习: 多线程编程基础 数据竞争:共享数据的问题 多线程共享内存是优势,也是问题的源头。当两个或多个线程同时访问同一内存位置,且至少有一个是写操作,就构成数据竞争(data race)。C++ 标准规定数据竞争是未定义行为(UB):程序可能输出任何结果,可能崩溃,也可能"碰巧正确"。 看一个经典例子——两个线程各做 100 万次 counter++: #include <iostream> #include <thread> int counter = 0; void add() { for (int i = 0; i < 1000000; ++i) { ++counter; // 数据竞争: 两个线程同时读写 counter } } int main() { std::thread t1(add); std::thread t2(add); t1.join(); t2.join();...
多线程编程进阶
需要预先学习: 多线程编程基础 数据共享问题与锁 线程间通信的问题 锁解决了"互斥"(同一时刻只有一个线程进临界区),但没解决"等待":线程 A 要等线程 B 完成某件事(比如数据准备好了)才能继续,A 应该怎么等? 忙等:while (!ready) {} 死循环空转,浪费 CPU,还带来数据竞争 轮询:while (!ready) { sleep(1ms); } 浪费少些,但有延迟,且 ready 的读写仍要同步 这两种方案都不优雅。标准答案有两个:条件变量(等待"事件")和 future/promise(等待"结果")。本篇文章围绕它们展开,最后实现一个线程池。 条件变量 std::condition_variable 条件变量(condition variable)让线程阻塞等待某个条件成立,由其他线程唤醒它。它必须配合 std::unique_lock(和一把互斥量)使用。 核心 API: 函数 行为 wait(lock) 阻塞,并自动解锁;被唤醒后...
原子操作
需要预先学习: 多线程编程基础 数据共享问题与锁 为什么要原子操作 回顾《数据共享问题与锁》中的计数器例子:两个线程各执行 100 万次 ++counter,结果不是 200 万。原因是 ++counter 在底层是"读 → 加 → 写"三步,两个线程的三步交错执行,更新互相覆盖。 用互斥锁可以解决,但代价不小:lock() 通常涉及系统调用(竞争时线程睡眠/唤醒)、缓存行失效、上下文切换。如果只是"把一个变量加 1"这种简单操作,锁是杀鸡用牛刀。 原子操作(atomic operation)就是答案:不可分割的操作——要么完整执行,要么完全不执行,中间不可能被其他线程插入。x = 1 这类单条机器指令天然是原子的;而"读-改-写"这类多步操作,需要硬件提供特殊指令(如 x86 的 lock 前缀指令)保证原子性。 std::atomic<T> 把这种能力封装成标准库类型。用它改写计数器: #include <atomic> #include <iostream> #incl...


