线程在对象销毁后仍可执行且未触发段错误的原因探究
为什么线程访问已销毁对象的成员函数却没崩溃?
这个现象其实是**未定义行为(Undefined Behavior)**的一种“幸运”表现——不是每次都能这么“正常”输出的,只是这次刚好没触发崩溃而已。咱们来一步步拆解背后的原因:
1. 先理清代码的执行时序
你代码里的执行顺序大概是这样的:
- main函数创建
shared_ptr<X>对象x,指向一个X实例。 - 启动线程,lambda捕获
x的引用,在线程里调用x->f():先输出f-1,然后sleep 5秒。 - main线程里调用
x->execute(),输出execute-1。 - main线程调用
x.reset(),此时shared_ptr的引用计数降到0,X对象被销毁,调用~X()输出DestrX。 - 5秒后,子线程的sleep结束,输出
f-2。 - 子线程执行完毕,main线程调用
join()后输出End。
2. 为什么没触发段错误?
核心原因在于:你在f()函数里没有访问X对象的任何成员变量。
成员函数的代码是存储在类的代码段里的,并不属于某个具体的对象。当你调用x->f()时,编译器会把它转换成类似X::f(this)的形式——这里的this指针指向当前X对象。但如果对象已经被销毁,this就变成了悬空指针:
- 如果
f()里没有使用this去访问对象的成员数据(比如没有读取/修改X的成员变量),那即使this悬空,函数依然能正常执行代码段里的逻辑(比如打印字符串、sleep),因为这些操作不需要访问对象的内存区域。 - 但如果
f()里有访问对象成员的代码(比如X里有int a;,然后f()里写cout << a << endl;),那此时访问悬空this指向的内存,大概率会触发段错误,或者输出随机值。
3. 关于lambda捕获的额外风险
你用[&]()捕获了x的引用,这其实是另一个隐藏的坑:如果x这个shared_ptr对象本身被销毁了(比如main函数提前结束),那lambda里的引用就会变成悬空引用,此时x->f()的调用会直接崩溃。不过在你的代码里,x是main函数的局部变量,main还在运行,所以x本身一直存在,只是它指向的X对象被销毁了,所以这次没触发这个问题。
正确的做法是什么?
要避免这种悬空访问的风险,应该让线程持有X对象的shared_ptr拷贝,也就是把lambda的捕获改成[x]()(值捕获):
thread_ = std::thread( [x]() { x->f(); });
这样shared_ptr的引用计数会增加1,即使main线程调用x.reset(),引用计数也不会降到0,X对象会等到子线程执行完毕、线程里的shared_ptr被销毁后才会被回收,从根本上避免悬空访问。
内容的提问来源于stack exchange,提问作者John Marek
相关产品推荐
相关产品推荐

