C++ 多线程入门导读
先建立一个总模型
多线程不是“多写几个 std::thread”。真正要理解的是下面五件事:
任务:要做什么
线程:谁来做
数据:任务之间共享什么
同步:什么时候等、什么时候通知
退出:怎么安全收尾如果只学 API,很容易变成:
- 会启动线程,但不知道什么时候
join。 - 会加锁,但不知道锁保护的是哪份数据。
- 会用条件变量,但不知道为什么必须写谓词。
- 会用
atomic,但误以为所有并发问题都能靠原子解决。 - 会写线程池,但不知道任务太小、队列太大、退出不完整会出问题。
所以学习顺序应该从“任务关系”开始,而不是从背函数开始。
一个最小例子:主线程等子线程
#include <iostream>
#include <thread>
void work() {
std::cout << "work in another thread\n";
}
int main() {
std::thread t(work);
t.join();
std::cout << "main done\n";
}这里有三个关键点:
std::thread t(work):创建一个系统线程去执行work。join():主线程等待子线程结束。join不是杀死线程,只是等待线程自己执行完。
如果 std::thread 析构时还没有 join 或 detach,程序会 std::terminate。所以线程生命周期是第一个必须掌握的知识。
继续看:cpp线程生命周期
为什么多线程会出错:共享数据
下面代码看起来只是多个线程一起给计数器加一:
#include <iostream>
#include <thread>
#include <vector>
int main() {
int counter = 0;
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&] {
for (int j = 0; j < 10000; ++j) {
++counter;
}
});
}
for (auto& t : threads) {
t.join();
}
std::cout << counter << '\n';
}你可能期待输出 40000,但实际不可靠。原因是 ++counter 不是一个不可分割的动作,它通常包含:
读 counter
加 1
写回 counter多个线程同时做这三步,就可能互相覆盖结果。这叫数据竞争,在 C++ 中属于未定义行为。
解决方式之一是加锁:
std::mutex m;
{
std::lock_guard<std::mutex> lock(m);
++counter;
}继续看:cpp互斥锁与RAII
锁到底保护什么
一个常见误区是:“我给一段代码加锁了,所以安全了。”
更准确的说法是:锁保护的是数据,不是代码。
例如:
std::mutex m;
std::vector<int> values;应该在脑子里建立规则:
m 保护 values
任何线程读写 values 前,都必须先拿到 m如果只有一部分代码拿锁,另一部分代码绕过锁直接访问 values,仍然不安全。
什么时候用条件变量
锁只能解决“同一时间只有一个线程改数据”。但很多时候,线程还需要等某个条件成立。
典型例子:队列为空时,消费者不应该一直空转占 CPU,而应该睡眠;生产者放入数据后再唤醒它。
生产者:放数据 -> 通知
消费者:没数据就睡 -> 被通知后再检查这就是 std::condition_variable 的场景。
最重要的写法:
cv.wait(lock, [&] {
return !q.empty() || done;
});必须带谓词,因为:
- 线程可能虚假唤醒。
- 通知可能早于等待。
- 多个消费者醒来后,数据可能被其他线程先取走。
继续看:cpp条件变量
消息传递比到处共享更容易理解
共享内存写法容易让所有线程都能改同一堆对象。项目变大后,谁改了数据很难追。
更容易维护的方式是让线程通过队列传任务或数据:
生产者线程 -> 队列 -> 消费者线程这样共享状态集中在队列里,队列内部用锁和条件变量保护。其他业务代码只负责 push 和 pop。
这就是 cppChannel模型 的价值。
future 解决什么问题
如果你只想启动一个异步任务,并拿到它的返回值,不一定要自己设计队列。
#include <future>
#include <iostream>
int compute() {
return 42;
}
int main() {
auto fut = std::async(std::launch::async, compute);
std::cout << fut.get() << '\n';
}future 表示“未来会有一个结果”。你用 get() 等它完成并取结果。
原子操作不是万能锁
std::atomic<int> 适合简单计数器、停止标志这类独立状态:
std::atomic<bool> stop = false;但如果一个操作需要同时维护多个变量的不变量,比如:
队列内容
队列大小
任务状态通常还是要用 mutex,否则很难保证整体一致。
继续看:cpp原子操作
为什么需要线程池
不要每来一个小任务就创建一个线程:
for (auto task : tasks) {
std::thread(task).detach();
}问题:
- 线程创建成本高。
- 线程数量可能失控。
- 退出和异常很难管理。
- CPU 会花大量时间在线程切换上。
线程池的思路是:
固定数量工作线程
任务进入队列
工作线程循环取任务执行继续看:cpp线程池
推荐学习顺序
第一阶段:先能写对
第二阶段:组织任务
第三阶段:控制性能
看代码时先问这 8 个问题
- 这个线程负责什么任务?
- 线程在哪里启动?
- 谁负责等待它结束?
- 它访问了哪些共享数据?
- 哪个锁保护这些共享数据?
- 它等待什么条件?
- 退出信号从哪里来?
- 异常或提前返回时资源是否能自动释放?
能回答这 8 个问题,多线程代码就不会只是“看起来会跑”,而是开始可维护。