本章目标:线程创建、数据竞争、互斥锁、条件变量、 原子操作、async/launch 策略、线程安全的正确姿势。 并发是 C++ 最容易被写错的部分,重点讲"如何不出错"。
#include <thread>
void worker(int id) {
std::println("线程 {} 开始", id);
}
std::thread t1(worker, 1); // 创建线程,立即开始跑
std::thread t2([]{ /* lambda 也行 */ });
t1.join(); // 等 t1 结束(join = 汇合)
t2.join();
关键规则(背下来):
直接 terminate 崩溃
危险:detach 的线程访问已销毁变量 = 悬空
传入引用参数要小心:
void f(int& x);
std::thread t(f, std::ref(x)); // 不写 std::ref 就是拷贝!
两个线程同时读写同一个变量 = 数据竞争 = 未定义行为。 结果不可预测(可能是崩溃、错误值、读一半的数据)。
int counter = 0;
// 两个线程各自 counter++ 一万次
// 期望 20000,实际经常是 19000 左右!
// 原因:counter++ 是"读-改-写"三步,两个线程交错执行
人话:counter++ 在 CPU 层面是三条指令 (读内存 → 加 1 → 写回内存)。 两个线程同时执行这三步会互相覆盖。 "原子性"被破坏 → 数据竞争 → 未定义行为。
预防方案(二选一):
#include <mutex>
int counter = 0;
std::mutex mtx;
void safe_inc() {
std::lock_guard<std::mutex> lock(mtx); // 上锁(RAII)
++counter; // 临界区
} // 出作用域自动解锁(RAII 保证异常安全)
lock_guard:构造上锁,析构解锁。永远用它,别手动 lock/unlock (手动容易忘记解锁或异常时不解锁 → 死锁/卡死)。
多个锁避免死锁的铁律:所有线程按**相同顺序**上锁。 (两个线程分别以"先 A 后 B"和"先 B 后 A"上锁 → 互相等待 → 死锁)
#include <atomic>
std::atomic<int> counter{0};
void worker() { ++counter; } // 原子自增,无锁!
// 10 个线程各 +1 万次 → 永远正好 100000
原子 = 硬件保证单条指令完成,不需要锁。 性能:比 mutex 快得多,但只能做简单操作。 ("读-改-写"类操作如 fetch_add 都是原子的)
什么时候用 atomic,什么时候用 mutex?
内存序(memory order)——进阶概念,新手先默认默认值:
std::atomic<int> x{0};
x.store(5); // 写
int v = x.load(); // 读
x.fetch_add(1); // 原子加
默认"顺序一致性",简单场景绝对够用。
场景:生产者线程生成数据,消费者线程等数据到了再处理。 轮询(忙等)浪费 CPU;条件变量让消费者"睡觉"直到被通知。
#include <condition_variable>
std::mutex cv_mtx;
std::condition_variable cv;
std::vector<int> queue;
bool done = false;
// 消费者线程:
{
std::unique_lock<std::mutex> lock(cv_mtx);
cv.wait(lock, [] { return !queue.empty() || done; });
// 等条件满足才继续(自动释放锁,被唤醒后重新上锁)
int item = queue.back(); // 处理数据
}
// 生产者线程:
{
std::lock_guard<std::mutex> lock(cv_mtx);
queue.push_back(item);
cv.notify_one(); // 叫醒一个等待者
}
关键点:
#include <future>
auto result = std::async(std::launch::async, [] {
// 耗时计算,另一个线程执行
int sum = 0;
for (int i = 1; i <= 1'000'000; ++i) sum += i;
return sum;
});
// 主线程做别的事...
std::println("结果: {}", result.get()); // 等它完成并取回结果
async 的好处:
future 只能 get 一次。多个任务并行 + 等全部:
std::vector<std::future<int>> futures;
for (int i = 0; i < 4; ++i)
futures.push_back(std::async(std::launch::async, f, i));
for (auto& fu : futures) results.push_back(fu.get()); // 依次取
例子:并行求和(分块,无共享写):
template <typename It>
long long parallel_sum(It begin, It end) {
auto n = std::distance(begin, end);
if (n <= 0) return 0;
const int nthreads = 4;
std::vector<std::future<long long>> futures;
for (int t = 0; t < nthreads; ++t) {
auto lo = begin + t * n / nthreads;
auto hi = begin + (t + 1) * n / nthreads;
futures.push_back(std::async(std::launch::async,
[lo, hi] {
return std::accumulate(lo, hi, 0LL);
}));
}
long long total = 0;
for (auto& fu : futures) total += fu.get();
return total;
}
死锁:两个线程互相等对方手里的锁 → 都永远卡住
症状:程序卡死不动
预防:统一锁顺序;用 std::scoped_lock 一次拿多个锁
// 同时拿两把锁(C++17):
std::scoped_lock lock(m1, m2); // 内部按固定顺序加锁,防死锁
活锁:线程反复让出资源,永远不进展(罕见) 数据竞争:未定义行为,症状千奇百怪(值不对、崩溃、随机结果)
std::thread::hardware_concurrency() 查核数
(阈值经验:任务 > 几十微秒才值得并发)
#include <execution>
std::sort(std::execution::par, v.begin(), v.end());
一行搞定并行排序(标准库自动分块调度)!
练习题(配套测试:测试_第22章_多线程.cpp)