背景
在 C++ 里,用 std::thread
启动一个新线程时,你会发现下面这个看似“传引用”的写法并不起作用:
int x = 42;
auto f = [](int& v){
// do something with v
};
std::thread t(f, x); // ❌ 这里其实把 x 传给 f 的是一个临时副本,不是引用!
背后主要有两个原因:
1. std::thread
的参数是“Decay-Copy”策略
std::thread
的构造模板大致长这样(简化版):
template <class F, class... Args>
explicit thread(F&& f, Args&&... args);
- 在内部,它并不是直接把
args...
当作引用来储存 - 而是对每个
arg
做std::decay_t<Arg>
的拷贝/移动 ——decay
会把引用类型剥掉,数组转指针,函数转函数指针- 最终在线程内部储存的,永远是一个值副本
所以,当你写 thread(f, x)
时,x
被 拷贝 了一份到线程内部,函数实际收到的是这个拷贝,不是原来的 x
。
2. C++ 设计上要避免“悬垂引用”
如果把一个局部变量的引用塞给线程,线程有可能比调用 std::thread
的作用域活得更久:
void spawn() {
int x = 100;
std::thread t(f, std::ref(x)); // f 拿到的是 x 的引用
t.detach();
} // x 在这里已经析构,f 里再用 v 就是悬空引用 → 未定义行为!
为了在绝大多数情况下保证安全,标准库选择了:
- 默认复制所有参数进线程闭包
- 如果你真要传引用,必须显式地告诉编译器“这是个引用”:
std::thread t(f, std::ref(x)); // 用 std::reference_wrapper<T>
- 这同时也提醒你:
- 线程里的代码在访问同一个对象时,必须自己用
std::mutex
、std::atomic
等同步; - 要确保被引用的对象在所有线程都结束前,仍然存活。
- 线程里的代码在访问同一个对象时,必须自己用
正确传引用的示例
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void worker(int& counter) {
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
int counter = 0;
std::thread t1(worker, std::ref(counter));
std::thread t2(worker, std::ref(counter));
t1.join();
t2.join();
std::cout << "最终 counter = " << counter << "\n"; // 2000
}
std::ref(counter)
:告诉std::thread
“不要 decay-copy,传counter
的引用”std::mutex
+lock_guard
:避免多个线程并发修改同一个变量导致的数据竞争- 确保
counter
活得够久:直到所有线程join()
完成
小结
- 默认:
std::thread
会把你传给它的每个参数按值复制/移动到内部闭包里。 - 要传引用:必须用
std::ref()
(或std::cref()
)显式包装,否则一切都会被“decay”成值; - 并发安全:即便拿到引用,也要做好同步(
mutex
/atomic
); - 生命周期:被引用对象必须在所有线程结束前一直存活,否则就会出现“悬垂引用”与未定义行为。