对象生命周期与多线程:已销毁对象仍被线程正常访问的原因
为什么销毁对象后线程还能正常操作它?
这是个典型的**未定义行为(Undefined Behavior)**场景,咱们一步步拆解背后的原因:
1. delete到底做了什么?
当你调用delete data时,操作系统只是收到了一个“这块内存现在可以被重新分配”的通知——它并不会立刻清空内存里的内容,也不会把指针标记为无效。在这块内存还没被其他程序/线程占用之前,原来的Data对象数据大概率还留在原地。你的工作线程此时去访问这块内存,碰巧还能读到旧值、甚至修改,所以看起来“一切正常”。
但这完全是运气,不是合法操作!
2. 为什么这极度危险?
未定义行为的可怕之处在于它的不可预测性:
- 如果操作系统在某个时刻把这块内存分配给了其他任务,你的线程再去读写就会直接触发**段错误(Segmentation Fault)**崩溃。
- 更隐蔽的情况是,你的线程修改了这块已释放的内存,刚好覆盖了其他程序的数据,会导致莫名其妙的bug,排查起来极其困难。
你的代码里,oops2()函数退出前就delete了Data对象,此时工作线程还在运行,data指针已经变成了悬空指针(Dangling Pointer),后续所有对它的操作都是非法的。
3. 正确的做法是什么?
要确保线程访问的对象生命周期至少和线程一样长,常见的解决方案有几种:
方案一:等待线程结束后再释放对象
调整oops2()的逻辑,把delete放到线程join之后:
void oops2() { Data* data = new Data(100); func2 my_func(data); my_thread = std::thread(my_func); my_thread.join(); // 先等线程执行完 delete data; // 再释放对象 }
方案二:用智能指针共享所有权
使用std::shared_ptr来管理Data对象,让主线程和工作线程共享所有权,直到最后一个使用者结束才自动释放内存:
#include <memory> class Data { /* 保持原定义不变 */ }; struct func2 { std::shared_ptr<Data> data; func2(std::shared_ptr<Data> data_): data(data_){} void operator()() { printf("Address of func2 data is %p\n", data.get()); for (int j=0;j < 1000000;++j) { print_val(data.get()); set_val(j, data.get()); } } }; void oops2() { auto data = std::make_shared<Data>(100); func2 my_func(data); my_thread = std::thread(my_func); // 无需手动delete,最后一个shared_ptr销毁时自动释放 }
方案三:把对象所有权转移给线程
让工作线程负责在执行结束后释放对象:
struct func2 { Data* data; func2(Data* data_): data(data_){} void operator()() { printf("Address of func2 data is %p\n", data); for (int j=0;j < 1000000;++j) { print_val(data); set_val(j, data); } delete data; // 线程执行完自行释放 } }; void oops2() { Data* data = new Data(100); func2 my_func(data); my_thread = std::thread(my_func); // 主线程不再delete }
总结
你看到的“正常运行”只是未定义行为的一种表现,绝对不能依赖这种情况。编写多线程代码时,必须严格保证线程访问的对象生命周期是安全的,避免悬空指针和未定义行为。
内容的提问来源于stack exchange,提问作者Oleg Kuzenko
相关产品推荐
相关产品推荐

