V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  C++

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

  •  
  •   amiwrong123 · 2020-10-21 16:05:27 +08:00 · 2283 次点击
    这是一个创建于 1498 天前的主题,其中的信息可能已经有所发展或是发生改变。

    墓碑内容是:

    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 的参数捕获有关系吗?我用的复制[=]()

    7 条回复    2020-10-27 22:41:34 +08:00
    lonewolfakela
        1
    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
        2
    wutiantong  
       2020-10-21 18:05:59 +08:00
    我觉得“null pointer dereference”是一个指向性蛮强的错误类型(相比于,比如:释放野指针),有希望从代码里抠出问题来。
    amiwrong123
        3
    amiwrong123  
    OP
       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
        4
    lonewolfakela  
       2020-10-22 09:31:36 +08:00   ❤️ 1
    @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
        5
    amiwrong123  
    OP
       2020-10-22 10:30:05 +08:00
    @lonewolfakela
    好吧,我这一猜想看来又失败了。现在这件事分为两件事:
    1. 调查 bug 出现的原因,所以我才分析这么一大堆。我再看看这代码,分析分析原因吧
    2. 给出解决方案,你说的直接使用 std::vector<std::function<void()>>(其实我也觉得是这样,完全没有必要使用这些东西,但代码开始不是我写的,但这个 bug 让我来调查,蛋疼啊),我也正在试,希望好使吧。

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

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

    然后排查的点我认为也应该从传值的对象开始检查,要确保这些对象都是“拷贝安全”的。比如说可能引起你这个问题的一个场景是:你自己写了一个类或者用了一个别人的类,然后用 lamda 捕获,但是在传值的过程中某个对象发生了浅拷贝,最后在析构过程中对同一资源处理了两遍。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1070 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 21:38 · PVG 05:38 · LAX 13:38 · JFK 16:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.