概念
C++ 里的线程,通常指一个进程内可以并发执行的独立执行流。标准库从 C++11 开始提供线程支持,核心类是 std::thread。你可以把它理解成:主线程之外,再开几个“工人”同时做事,比如并行计算、后台日志、网络收发、任务调度。
但线程不是“免费提速”。它会带来几个核心问题:
- 线程如何创建和回收
- 多个线程同时访问同一份数据时,如何避免数据竞争
- 线程之间如何通信和等待
- 怎么优雅停止线程,避免资源泄漏或程序崩溃
下面按这条线讲。
一、最基础:创建线程
最常见的写法是把一个函数交给 std::thread 执行。
#include <iostream>
#include <thread>
void worker() {
std::cout << "worker thread id = " << std::this_thread::get_id() << '\n';
}
int main() {
std::thread t(worker);
std::cout << "main thread id = " << std::this_thread::get_id() << '\n';
t.join(); // 等待子线程执行完成
return 0;
}
这里有几个关键点:
- std::thread t(worker); 会立刻启动一个新线程。
- t.join(); 表示主线程等待 t 执行完。
- 如果一个 std::thread 对象在析构前既没有 join,也没有 detach,程序会直接 terminate。
所以这段代码里,join 不是“建议”,而是必须处理。
二、join 和 detach 的区别
线程启动后,必须在这两个里选一个:
- join
当前线程等待目标线程结束 - detach
目标线程脱离管理,后台独立运行
示例:
#include <iostream>
#include <thread>
#include <chrono>
void task() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "task finished\n";
}
int main() {
std::thread t(task);
// 方案1:等待结束
t.join();
// 方案2:后台运行
// t.detach();
return 0;
}
detach 要慎用。因为一旦 detach:
- 你无法再 join 它
- 很难控制它的生命周期
- 如果它访问了已经销毁的对象,容易出严重 bug
实际开发里,大多数情况优先用 join,或者用 C++20 的 std::jthread。
三、线程传参
std::thread 默认会把参数按值拷贝到新线程里。
#include <iostream>
#include <thread>
void printValue(int x) {
std::cout << "x = " << x << '\n';
}
int main() {
int a = 10;
std::thread t(printValue, a);
t.join();
return 0;
}
如果你想传引用,必须显式用 std::ref。
#include <iostream>
#include <thread>
#include <functional>
void addOne(int& x) {
++x;
}
int main() {
int a = 10;
std::thread t(addOne, std::ref(a));
t.join();
std::cout << "a = " << a << '\n'; // 11
return 0;
}
如果不加 std::ref,线程拿到的是副本,不会改到原变量。
也可以直接用 lambda:
#include <iostream>
#include <thread>
int main() {
int a = 42;
std::thread t([a]() {
std::cout << "a = " << a << '\n';
});
t.join();
return 0;
}
如果 lambda 用引用捕获,也要保证被引用对象在线程执行期间仍然有效。
四、最常见问题:数据竞争
多个线程同时读写同一份数据,如果没有同步,就是数据竞争。结果是未定义行为。
错误示例:
#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';
return 0;
}
你可能以为结果一定是 200000,但实际经常不是。因为 ++counter 不是原子操作,它通常会分成读、改、写三个步骤,两个线程会互相踩。
五、用 mutex 保护共享数据
解决共享数据竞争,最常用的是互斥锁 std::mutex。
#include <iostream>
#include <thread>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "counter = " << counter << '\n';
return 0;
}
这里推荐用 std::lock_guard,而不是手动调用 mtx.lock() / mtx.unlock()。原因很简单:
- 自动释放锁
- 遇到异常也不会忘记 unlock
- 写法更安全
等价但不推荐的写法是:
mtx.lock();
++counter;
mtx.unlock();
这种方式一旦中途 return 或抛异常,就容易死锁。
六、什么时候用 atomic 而不是 mutex
如果只是做简单计数、自增、自减、状态位修改,可以用 std::atomic,通常比 mutex 更轻量。
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<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';
return 0;
}
选择原则很简单:
- 简单单变量同步,用 atomic
- 复杂临界区、多个变量一致性,用 mutex
atomic 不是 mutex 的全替代品。
七、线程等待条件:condition_variable
如果一个线程要“等某个条件成立再继续”,不要用死循环一直检查,这会浪费 CPU。应该用条件变量。
典型场景:生产者消费者。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
using namespace std;
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;
void producer() {
for (int i = 1; i <= 10000; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
cout << "producer " << i << endl;
}
cv.notify_one();
cout << "producer notify_one: " << i << endl;
}
{
std::lock_guard<std::mutex> lock(mtx);
finished = true;
cout << "producer finished\n";
}
cv.notify_one();
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] {
cout << "consume wait" << endl;
return !q.empty() || finished;
});
while (!q.empty()) {
int value = q.front();
q.pop();
cout << "consume: " << value << ", left: " << q.size() << endl;
}
if (finished) {
cout << "consumer finished\n";
break;
}
} // end while
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
这里要注意两点:
- wait 最好带条件谓词
- 不能假设被唤醒后条件一定满足,因为可能有“虚假唤醒”
所以推荐这种形式:
cv.wait(lock, [] { return 条件成立; });
而不是只写:
cv.wait(lock);
八、std::lock_guard 和 std::unique_lock 的区别
两者都能管理锁,但定位不同:
- std::lock_guard
最轻量,作用域内自动加锁解锁 - std::unique_lock
更灵活,可以延迟加锁、手动解锁、配合 condition_variable 使用
示例:
std::mutex mtx;
void f1() {
std::lock_guard<std::mutex> lock(mtx);
// 临界区
}
void f2() {
std::unique_lock<std::mutex> lock(mtx);
// 临界区
lock.unlock();
// 非临界区
}
一般规则:
- 简单加锁,优先 lock_guard
- 需要和条件变量配合,或者需要手动 unlock,再用 unique_lock
九、成员函数作为线程入口
线程函数也可以是类成员函数,但需要额外传 this。
#include <iostream>
#include <thread>
class Task {
public:
void run(int x) {
std::cout << "run: " << x << '\n';
}
};
int main() {
Task task;
std::thread t(&Task::run, &task, 123);
t.join();
return 0;
}
格式是:
- 成员函数指针
- 对象地址或对象引用包装
- 成员函数参数
十、C++20 更推荐:std::jthread
如果编译器支持 C++20,很多时候 std::jthread 比 std::thread 更好用,因为它析构时会自动 join,不容易忘。
#include <iostream>
#include <thread>
#include <chrono>
void worker() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "worker done\n";
}
int main() {
std::jthread t(worker);
std::cout << "main end\n";
return 0; // t 析构时自动 join
}
它还支持停止请求 stop_token,这是现代 C++ 做线程取消的推荐方式。
#include <iostream>
#include <thread>
#include <chrono>
void worker(std::stop_token st) {
while (!st.stop_requested()) {
std::cout << "working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(300));
}
std::cout << "stopped\n";
}
int main() {
std::jthread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop();
return 0;
}
如果你在写新项目,优先考虑 std::jthread。
十一、线程池和 std::thread 的关系
std::thread 是“直接创建一个系统线程”。如果任务很多,频繁创建销毁线程成本会比较高。这时更常见的工程方案是线程池:
- 预先创建固定数量线程
- 任务来了放进队列
- 工作者线程不断从队列取任务执行
标准库本身没有正式线程池类型,所以企业项目里通常会自己实现或用第三方库。也就是说:
- 少量线程控制,用 std::thread
- 大量小任务调度,用线程池更合适
十二、常见坑
1. 忘记 join 或 detach
std::thread t(worker);
// 函数结束直接退出,t 析构时 terminate
这是最常见错误之一。
2. detach 后访问悬空对象
void f() {
int x = 10;
std::thread([&]() {
std::cout << x << '\n';
}).detach();
}
这里线程可能在 f 返回后才执行,x 已经销毁,属于悬空引用。
3. 多线程输出混乱
多个线程同时往 std::cout 写,输出可能交错。解决方式通常是:
- 给输出加锁
- 先拼接到字符串,再统一输出
- 用线程安全日志库
4. 死锁
例如两个线程以不同顺序拿两把锁:
// 线程1: 先锁 A,再锁 B
// 线程2: 先锁 B,再锁 A
这非常容易死锁。常见规避方法:
- 统一加锁顺序
- 用 std::scoped_lock 同时锁多个 mutex
示例:
#include <mutex>
std::mutex m1, m2;
void safe() {
std::scoped_lock lock(m1, m2);
// 同时安全锁住两把锁
}
十三、一个稍完整的例子:多线程处理任务
下面这个例子演示两个工作线程,从任务队列中取数据并处理。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <chrono>
class TaskQueue {
public:
void push(int task) {
{
std::lock_guard<std::mutex> lock(mtx_);
tasks_.push(task);
}
cv_.notify_one();
}
bool pop(int& task) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] {
return !tasks_.empty() || stopped_;
});
if (tasks_.empty()) {
return false;
}
task = tasks_.front();
tasks_.pop();
return true;
}
void stop() {
{
std::lock_guard<std::mutex> lock(mtx_);
stopped_ = true;
}
cv_.notify_all();
}
private:
std::queue<int> tasks_;
std::mutex mtx_;
std::condition_variable cv_;
bool stopped_ = false;
};
void worker(TaskQueue& queue, int workerId) {
int task;
while (queue.pop(task)) {
std::cout << "worker " << workerId
<< " processing task " << task << '\n';
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "worker " << workerId << " exit\n";
}
int main() {
TaskQueue queue;
std::vector<std::thread> workers;
for (int i = 0; i < 2; ++i) {
workers.emplace_back(worker, std::ref(queue), i + 1);
}
for (int i = 1; i <= 10; ++i) {
queue.push(i);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
queue.stop();
for (auto& t : workers) {
t.join();
}
return 0;
}
这个例子里包含了线程编程的几个核心要素:
- 多线程并发执行
- 共享任务队列
- mutex 保护共享数据
- condition_variable 阻塞等待任务
- stop 信号控制退出
- join 回收线程
这已经是很多服务端程序的基本模型了。
十四、怎么理解线程安全
“线程安全”不是一句空话,它通常意味着:
- 多个线程同时调用同一段代码,不会破坏数据一致性
- 不会出现未定义行为
- 结果符合预期
比如下面这个类不是线程安全的:
class Counter {
public:
void increment() {
++value_;
}
int get() const {
return value_;
}
private:
int value_ = 0;
};
因为多个线程同时 increment 会竞争。改成这样才更安全:
#include <mutex>
class Counter {
public:
void increment() {
std::lock_guard<std::mutex> lock(mtx_);
++value_;
}
int get() const {
std::lock_guard<std::mutex> lock(mtx_);
return value_;
}
private:
mutable std::mutex mtx_;
int value_ = 0;
};
十五、学习线程时的建议路线
建议按这个顺序掌握,不要一上来就碰复杂并发模型:
- 先会用 std::thread 创建线程
- 理解 join 和 detach
- 理解共享数据和数据竞争
- 学会 mutex、lock_guard、unique_lock
- 学会 condition_variable
- 再去看 atomic、future、async、jthread、线程池
如果前面没吃透,后面基本都是“看起来懂了,实际写就出 bug”。
十六、编译说明
如果你用 GCC 或 Clang,通常需要开启 C++11 及以上,并加线程选项:
g++ main.cpp -std=c++17 -pthread
如果你用 MSVC,通常这样即可:
cl /EHsc /std:c++17 main.cpp
如果用到 std::jthread,需要 C++20:
g++ main.cpp -std=c++20 -pthread
总结
C++ 线程的核心不是“怎么开线程”,而是“怎么正确管理并发”。真正要掌握的是这几个点:
- 线程生命周期:创建、join、停止
- 共享数据同步:mutex、atomic
- 线程通信:condition_variable
- 资源安全:RAII,避免悬空引用和死锁
- 新代码优先考虑 C++20 的 std::jthread
十七、C++ thread 面试题
-
std::thread 和进程的区别是什么
线程是进程内的执行流,同一进程下的线程共享地址空间和大多数资源;进程之间默认隔离更强,切换和通信成本通常更高。 -
std::thread 创建后为什么必须 join 或 detach
因为线程对象析构时如果仍然处于 joinable 状态,程序会直接调用 std::terminate。也就是说线程生命周期必须显式处理。 -
join 和 detach 的区别
join 是等待线程结束,并回收线程相关资源。
detach 是让线程独立运行,当前对象不再管理它。
工程里通常优先 join,因为 detach 很容易引入悬空引用和失控生命周期问题。 -
什么是数据竞争
多个线程在没有同步的情况下,同时访问同一块内存,并且至少有一个是写操作,这就是数据竞争,结果属于未定义行为。 -
mutex 和 atomic 怎么选
atomic 适合单个变量的简单原子读写、自增、自减、状态位。
mutex 适合保护一段临界区,或者多个变量的一致性。
一句话:简单共享状态用 atomic,复杂共享状态用 mutex。 -
lock_guard 和 unique_lock 的区别
lock_guard 更轻量,只负责作用域加锁解锁。
unique_lock 更灵活,可以延迟加锁、提前解锁、转移所有权,还能配合 condition_variable 使用。 -
condition_variable 的作用
让线程在条件不满足时睡眠等待,而不是忙等。适合生产者消费者、任务队列、事件通知等场景。 -
为什么 wait 一般要配合谓词使用
因为存在虚假唤醒。正确写法是“醒来以后重新检查条件”,标准库已经把这个模式封装在 wait(lock, predicate) 里了。 -
什么是死锁,怎么避免
两个或多个线程彼此等待对方释放锁,导致永久阻塞。
常见避免方式: -
统一加锁顺序
-
一次性锁多把锁,比如 scoped_lock
-
缩小临界区
-
尽量避免锁嵌套
-
线程池为什么比频繁 new thread 更合适
频繁创建销毁线程成本高。线程池通过复用固定数量的工作线程,从任务队列取任务执行,能显著降低调度开销,适合大量短任务。 -
future、promise、packaged_task 分别是什么
future:拿结果
promise:设置结果
packaged_task:把“可调用对象”包装成可异步执行并可通过 future 取结果的任务 -
C++20 的 jthread 比 thread 好在哪里
jthread 析构时自动 join,而且支持 stop_token,更适合现代 C++ 的可控退出模型。 -
下面代码有没有问题
int x = 0;
std::thread t(& { ++x; });
t.detach();
有风险。因为如果外层作用域结束,x 被销毁,而后台线程还没执行完,就会访问悬空引用。
-
多线程下 cout 为什么会乱
因为多个线程同时输出时,字符流可能交叉写入。解决方式通常是给输出加锁,或者使用线程安全日志系统。 -
什么叫线程安全
多个线程并发调用时,不会发生数据竞争,不会破坏对象状态,结果仍然正确。
十八、简易线程池完整代码
下面这个版本基于 C++17,可直接提交任务,并拿到 future 结果。
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
#include <chrono>
class ThreadPool {
public:
explicit ThreadPool(std::size_t threadCount) : stop_(false) {
if (threadCount == 0) {
throw std::invalid_argument("threadCount must be greater than 0");
}
workers_.reserve(threadCount);
for (std::size_t i = 0; i < threadCount; ++i) {
workers_.emplace_back([this] {
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) {
return;
}
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mutex_);
stop_ = true;
}
condition_.notify_all();
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
}
template <class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>> {
using ReturnType = std::invoke_result_t<F, Args...>;
auto taskPtr = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<ReturnType> result = taskPtr->get_future();
{
std::lock_guard<std::mutex> lock(mutex_);
if (stop_) {
throw std::runtime_error("enqueue on stopped ThreadPool");
}
tasks_.emplace([taskPtr] {
(*taskPtr)();
});
}
condition_.notify_one();
return result;
}
private:
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable condition_;
bool stop_;
};
int slowSquare(int x) {
std::this_thread::sleep_for(std::chrono::milliseconds(300));
return x * x;
}
int main() {
ThreadPool pool(4);
std::vector<std::future<int>> results;
for (int i = 1; i <= 8; ++i) {
results.push_back(pool.enqueue(slowSquare, i));
}
for (std::size_t i = 0; i < results.size(); ++i) {
std::cout << "task " << i + 1 << " result = "
<< results[i].get() << '\n';
}
auto sumFuture = pool.enqueue([](int a, int b) {
return a + b;
}, 10, 20);
std::cout << "sum = " << sumFuture.get() << '\n';
return 0;
}
这份线程池怎么工作
先看整体结构:
-
workers_
保存一组工作线程。 -
tasks_
任务队列,里面每个元素都是一个无参函数对象,也就是 std::function<void()>。 -
mutex_ + condition_
保护任务队列,并让工作线程在“没有任务”时休眠等待。 -
stop_
线程池停止标志。析构时会把它设为 true,然后唤醒所有工作线程退出。
工作线程的核心逻辑是一个死循环:
- 先拿锁
- 如果任务队列为空,就用 condition_variable 等待
- 醒来后,如果 stop_ 为 true 且任务也空了,就退出线程
- 否则取出一个任务执行
- 回到循环继续等下一个任务
也就是说,线程池的本质就是:
- 固定数量线程常驻
- 任务不断入队
- 空闲线程不断出队执行
为什么 enqueue 返回 future
因为很多时候你不只是想“扔个任务过去”,你还想拿它的返回值。
例如:
auto future = pool.enqueue([] {
return 42;
});
int answer = future.get();
这里的关键是 std::packaged_task:
- 它把一个函数包装成“将来可拿结果”的任务
- get_future 可以取到对应 future
- 任务真正执行后,返回值会自动写入 future
所以线程池里做的是:
- 把用户提交的函数和参数绑定起来
- 包装成 packaged_task
- 再把这个 packaged_task 放进任务队列
- 工作线程执行它
- 用户通过 future.get() 取结果
几个容易问到的实现细节
-
为什么任务队列存的是 std::function<void()>
因为这样最统一。无论原始任务有没有参数、有没有返回值,最后都可以包装成“一个无参可执行单元”塞进队列。 -
为什么取任务时要先解锁再执行
因为任务执行可能很慢。如果一直持有锁,别的线程就无法入队或出队,整个线程池几乎退化成串行。 -
为什么析构时先设 stop_,再 notify_all
因为要让所有等待中的线程都醒来,检查退出条件并结束。 -
为什么 stop_ 和 tasks_ 的判断要放在同一个锁保护下
否则会发生竞态,可能导致线程漏掉任务或者错误退出。
这个实现的边界
这个版本适合学习和中小型使用,但还不是工业级完整版本。它还没有覆盖这些高级能力:
- 任务优先级
- 动态扩缩容
- 有界队列和拒绝策略
- 取消任务
- 工作窃取
- 统计监控
- 异常上报策略
不过作为面试题和基础实现,这个版本已经足够标准。
面试里怎么讲线程池
你可以用这套表达,比较稳:
线程池本质上是“线程复用 + 任务队列”。
核心目标是避免频繁创建销毁线程的开销。
实现上一般包含三部分:工作线程集合、任务队列、同步原语。
工作线程不断从队列中取任务执行;队列为空时通过条件变量阻塞等待;线程池销毁时通过停止标志和通知机制让所有线程有序退出。
如果需要返回值,通常结合 packaged_task 和 future 实现。

3447

被折叠的 条评论
为什么被折叠?



