4、多线程基础:线程的创建与销毁,线程同步的必要性,数据竞争的概念
多线程,说白了就是让程序同时干好几件事。你想想看,一个单线程的程序,就像只有一个工人的流水线。活儿得一件一件干,遇到慢的操作(比如读文件、等网络),整个生产线就卡住了。多线程就是多招几个工人,各干各的,效率自然就上来了。
但多线程不是请客吃饭。我这些年调试过的崩溃、死锁、数据错乱,十有八九都跟多线程有关。嗯,咱们今天就把基础打牢。
4.1 线程的创建与销毁
C++11 标准库提供了 std::thread,用起来非常直接。我个人习惯用 lambda 表达式来封装线程逻辑,代码更紧凑,也更容易看清上下文。
#include <iostream>
#include <thread>
void worker(int id) {
std::cout << "线程 " << id << " 开始工作\n";
// 模拟一些计算
for (int i = 0; i < 1000000; ++i);
std::cout << "线程 " << id << " 结束\n";
}
int main() {
std::thread t1(worker, 1);
std::thread t2([&]() { worker(2); });
// 等待线程结束
t1.join();
t2.join();
std::cout << "主线程结束\n";
return 0;
}
这里有两个关键操作:join() 和 detach()。
- join():主线程阻塞,等待子线程执行完毕。这是最安全的方式。
- detach():让子线程在后台独立运行,主线程不再管理它。我建议你慎用 detach。为什么?因为一旦 detach,你就失去了对线程的控制权。如果线程还在跑,主线程却退出了,程序可能直接崩溃。
std::thread 的析构函数会调用 std::terminate(),程序直接挂掉。我曾经在代码审查时见过有人忘了这茬,结果线上服务不定期崩溃,查了两天才找到原因。
4.2 线程同步的必要性
多个线程同时访问同一个数据,问题就来了。我给你看个经典例子:
#include <iostream>
#include <thread>
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 这行不是原子操作!
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter = " << counter << "\n";
// 期望值:200000
// 实际值:可能只有 120000 左右
return 0;
}
你运行一下试试。期望结果是 200000,但实际往往只有十几万。为什么会这样?
因为 ++counter 在 CPU 层面是三条指令:读内存、加1、写回内存。两个线程同时执行这三步,就可能出现「读到了旧值,各自加1,写回旧值+1」的情况。结果就是明明加了两次,counter 只增加了1。这就是典型的数据竞争。
核心概念: 当多个线程同时访问同一块内存,且至少有一个线程在写,而没有任何同步机制时,就发生了数据竞争。数据竞争会导致未定义行为——程序可能崩溃、结果错误、甚至产生诡异的逻辑跳转。
4.3 数据竞争的概念
数据竞争不是「偶尔出错」,而是「一定会出错」。只是出错的时间和表现方式不同。我在项目中遇到过最诡异的一次:一个多线程程序跑了三天才崩溃,而且每次崩溃的堆栈都不一样。最后用 ThreadSanitizer 一查,发现是一个全局变量在多个线程里无锁读写。
数据竞争有几个典型特征:
- 结果不可预测:同样的输入,每次运行结果可能不同。
- 难以复现:调试模式下可能正常,Release 模式下崩溃。
- 时序敏感:加个打印语句,问题就消失了(这叫 Heisenbug)。
要解决数据竞争,核心思路就是互斥——同一时间只允许一个线程访问共享数据。C++ 提供了 std::mutex 和 std::lock_guard:
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx;
void safe_increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter; // 现在安全了
}
}
int main() {
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "counter = " << counter << "\n";
// 稳定输出 200000
return 0;
}
std::lock_guard 是个好东西。它在构造时自动加锁,析构时自动解锁。就算中间抛出异常,锁也会被正确释放。我建议你养成习惯:能用 lock_guard 就别手动 lock/unlock。
💡 小技巧: 锁的粒度要尽量小。只锁必要的代码行,不要在锁里做耗时的 I/O 操作。否则其他线程会等很久,性能直线下降。
4.4 知识体系总览
下面这张图帮你理清本章的核心逻辑:
这张图把本章的逻辑串起来了。你从「线程创建与销毁」出发,多个线程访问「共享数据」,如果没有同步,就会掉进「数据竞争」的坑里。解决方案就是「线程同步」——用互斥锁、原子操作或条件变量来保护共享数据。
一句话总结: 多线程编程的核心就两件事——创建线程和保护数据。创建线程很简单,保护数据才是真正的技术活。
我曾经接手过一个遗留系统,里面全是裸的全局变量,没有任何锁。每次发布新版本,测试团队都要跑一周的压力测试,就为了抓那些随机崩溃。后来我花了两周时间,把所有共享数据都加上了 std::mutex,崩溃率直接降到零。嗯,这就是同步的价值。
下一节我们会深入讲互斥锁的各种用法和陷阱。今天先把基础打牢,多线程的世界才刚刚打开。