墓碑挂在 std::future 的析构函数,提示空指针异常,这个 bug 好久了求大佬帮忙看看

2020-10-21 16:05:27 +08:00
 amiwrong123

墓碑内容是:

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x10
Cause: null pointer dereference

backtrace:
    #00 pc 0000000000069568  /system/lib64/vndk-sp-28/libc++.so (std::__1::future<void>::~future()+40)
    #01 pc 000000000000a6ec  /vendor/lib64/xxx.so(我自己的 so) (android::BatchingConsumer<std::__1::shared_future<void>>::runInternal(std::__1::function<void (std::__1::vector<std::__1::shared_future<void>, std::__1::allocator<std::__1::shared_future<void>>> const&)> const&)+276)
    #02 pc 000000000000ab4c  /vendor/lib64/xxx.so (_ZNSt3__114__thread_proxyINS_5tupleIJNS_10unique_ptrINS_15__thread_structENS_14default_deleteIS3_EEEEMN7android16BatchingConsumerINS_13shared_futureIvEEEEFvRKNS_8functionIFvRKNS_6vectorISA_NS_9allocatorISA_EEEEEEEEPSB_SK_EEEEEPvSR_+68)
    #03 pc 0000000000082fec  /system/lib64/libc.so (__pthread_start(void*)+36)
    #04 pc 000000000002337c  /system/lib64/libc.so (__start_thread+68)

但是看了半天,总觉得是不可能挂在析构函数的啊。而且这个问题也不是必现的,大部分时候机能都能正常工作。但一旦发生了这个墓碑,机器就会重启,就很严重。

关于 future 的使用过程也很简单:

		std::shared_future<void> _future = std::async(std::launch::deferred,
			[=]() -> void {
			//balabala
		});

就是使用一个 future 来包装一个 lambda 表达式(这里没有使用到 future 的异步功能,因为是 deferred ),然后将这个 future 放到一个消息队列里面去,然后与这个消息队列相关的有一个唯一消费者线程会被唤醒,然后这个线程将这个 future 对象出队,执行_future.wait(),因为消费者的 run 方法就是一个死循环不断出队 task 对象(也就是我们的 future 对象),所以在死循环的最后的大括号那里,会自动执行这个 future 对象的析构函数。

上面的过程大多数时候都好使,但有的时候就会出现墓碑,这种问题到底可能是什么造成的啊?如果对象为 null,那么在执行_future.wait()的时候就应该报错啊,但现在却是在析构函数的时候报错。

看了网上相关内容,可能也就这个有关系:

https://zhuanlan.zhihu.com/p/39757902 提到的 std::async 会抓走所有异常。 https://blog.csdn.net/weixin_34256074/article/details/89412245 的匿名 std::thread 对象(感觉这个也和我这个没有关系)。

刚又想到一点,难道跟 lambda 的参数捕获有关系吗?我用的复制[=]()

2167 次点击
所在节点    C++
7 条回复
lonewolfakela
2020-10-21 17:30:05 +08:00
虽然不知道是为啥,但是有一点很有趣的就是,你这个报错的好像是 std::future 的析构函数而不是 std::shared_future 的析构函数。似乎 std::async 返回的 std::future 在已经被 moved 的情况下,析构的时候却爆炸了。
你这个场景里有什么必须使用 std::shared_future 的必要么?能不能直接用 std::future ?说不定避开“析构已经 moved 的 std::future”就能绕过这个问题?
wutiantong
2020-10-21 18:05:59 +08:00
我觉得“null pointer dereference”是一个指向性蛮强的错误类型(相比于,比如:释放野指针),有希望从代码里抠出问题来。
amiwrong123
2020-10-21 21:39:52 +08:00
@lonewolfakela
老哥,你说的话提醒了我,我能问你个问题吗,就是我这段代码,是 std::async 会返回一个 std::future 对象,然后赋值给一个 std::shared_future 对象,所以我这个 std::shared_future 对象持有一个 std::future 对象,我这么理解没错把。

但如果我把这个 std::shared_future 对象使用等于=符号赋值给另一个新的 std::shared_future 对象的话(因为我发现我程序里确实会发生这种事。。),是不是发生的浅拷贝,就是说,这两个 std::shared_future 对象持有的 std::future 对象是同一个,所以这两个 std::shared_future 对象析构时,会析构同一个 std::future 对象两次,所以就会有墓碑。我这么分析对吗?

但是我试验如下代码:
```
int main()
{
{
std::shared_future<void> _future = std::async(std::launch::deferred,
[=]() -> void {
//balabala
int a = 1;
});

std::shared_future<void> _future2 = _future;
}//到这个大括号会析构的
}
```
发现 VS 里面并没有报异常,哎,奇怪了
lonewolfakela
2020-10-22 09:31:36 +08:00
@amiwrong123 “我这么理解没错把。” —— 不完全正确。async 函数会返回一个 future 类型的右值,然后这个右值触发了 shared_future 的一个构造函数 [shared_future( future<T>&& other ) noexcept] ;此时 future 的内部状态会被移动到 shared_future 里——如果你对 c++的“移动”这一概念不太熟悉的话,你可以简单理解为 future 的某些成员变量被先拷贝到 shared_future 里面,然后 future 里这些已经被拷贝过的变量就全部清空为 null 了。所以正常情况下,这个被移动过的 future 在析构的时候会检查自己的成员变量,发现是 null,就不会做 delete 之类的操作。
将一个 shared_future 拷贝给另一个 shared_future 的话,默认执行的则不是移动而是拷贝构造 /赋值函数。shared_future 内的“状态”(通常是一个指针)确实会发生浅拷贝,但是在浅拷贝的同时还会有一个引用计数+1 的操作; shared_future 在析构的时候是会检查引用计数的,所以并不应该发生错误 delete 两次的问题。
事实上 future 和 shared_future 之间的关系有点类似于智能指针里的 unique_ptr 和 shared_ptr,你可以类比着理解一下。

另外我又仔细想了想你这个应用场景,感觉你完全用不着用 async 和 future 这套东西啊。你的消息队列完全只需要是一个 std::vector<std::function<void()>>就可以了。你这里用 async 只会增加 debug 难度……
amiwrong123
2020-10-22 10:30:05 +08:00
@lonewolfakela
好吧,我这一猜想看来又失败了。现在这件事分为两件事:
1. 调查 bug 出现的原因,所以我才分析这么一大堆。我再看看这代码,分析分析原因吧
2. 给出解决方案,你说的直接使用 std::vector<std::function<void()>>(其实我也觉得是这样,完全没有必要使用这些东西,但代码开始不是我写的,但这个 bug 让我来调查,蛋疼啊),我也正在试,希望好使吧。

总之,谢谢老哥的认真回答啦
Wirbelwind
2020-10-22 11:59:35 +08:00
@amiwrong123 Cause: null pointer dereference 指的是*nullptr,这样的报错。

优先检查*和其他会析构指针的地方比较好
liam0x801
2020-10-27 22:41:34 +08:00
首先非常不建议用来[=]捕获变量,这样会导致代码的可读性变差,比较推荐的方法是用到什么就把对应的变量声明出来。

然后排查的点我认为也应该从传值的对象开始检查,要确保这些对象都是“拷贝安全”的。比如说可能引起你这个问题的一个场景是:你自己写了一个类或者用了一个别人的类,然后用 lamda 捕获,但是在传值的过程中某个对象发生了浅拷贝,最后在析构过程中对同一资源处理了两遍。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/717146

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX