C++编程指南21 - 线程detach后其注意变量的生命周期
一:概述
如果一个线程被 detach()
了,那么它的生命周期将独立于创建它的作用域。因此,该线程只能安全地访问:
- 全局变量(global/static objects)
- 堆上分配的对象(free-store allocated objects,即
new
/make_unique()
创建的对象) 其他作用域的对象可能会在线程访问它们之前被销毁,导致 悬空指针(dangling pointer) 和 未定义行为。
二:示例代码
下面以一段代码来介绍下前面说的这种情况:
#include <iostream>
#include <thread>
#include <memory>
void f(int* p) {
*p = 99; // 可能访问悬空指针
}
int glob = 33; // 全局变量
void some_fct(int* p) {
int x = 77;
std::thread t0(f, &x); // 错误!x 是局部变量
std::thread t1(f, p); // 错误!p 可能指向局部变量
std::thread t2(f, &glob); // OK!glob 是全局变量
auto q = std::make_unique<int>(99);
std::thread t3(f, q.get()); // 错误!q 在 some_fct 结束时销毁
t0.detach();
t1.detach();
t2.detach();
t3.detach();
}
在这段代码中,线程detach后,会有以下几种情况发生:
-
t0(f, &x);
和t1(f, p);
x
是some_fct()
的局部变量,p
可能指向局部变量。t0
和t1
可能会在some_fct()
结束后访问已销毁的变量,导致 未定义行为。
-
t3(f, q.get());
q
是make_unique<int>(99)
创建的智能指针,q.get()
获取的指针在some_fct()
结束时无效。std::unique_ptr
在函数返回时析构,导致t3
访问无效内存。
-
t2(f, &glob);
- 由于
glob
是全局变量,它的生命周期足够长,可以安全地被t2
访问。
- 由于
三:解决方案
如何避免线程detach后的空悬指针和未定义行为?有以下两种方案:
1. 只使用全局变量或堆分配的对象
#include <iostream>
#include <thread>
#include <memory>
void f(int* p) {
*p = 99;
}
int glob = 33;
void some_fct(int* p) {
auto q = new int(99); // 使用堆对象
std::thread t1(f, &glob); // OK,全局变量
std::thread t2(f, q); // OK,堆上分配的对象
t1.detach();
t2.detach();
// 需要手动释放 q,避免内存泄漏
delete q;
}
2. 避免 detach()
,使用 std::thread::join()
或 joining_thread
void some_fct() {
int x = 77;
std::thread t(f, &x); // OK,因为 t.join() 之前 x 一直有效
t.join(); // 等待线程结束,确保不会访问销毁的对象
}
class joining_thread {
std::thread t;
public:
template <typename... Args>
explicit joining_thread(Args&&... args) : t(std::forward<Args>(args)...) {}
~joining_thread() {
if (t.joinable()) {
t.join();
}
}
};
void some_fct() {
int x = 77;
joining_thread t(f, &x); // OK
}