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

请教一个 shared_ptr 内存泄漏的问题

  •  
  •   guang19 · 2022-05-10 11:19:39 +08:00 · 2064 次点击
    这是一个创建于 929 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题,我正在用 cpp 写一个线程池的 submit 函数,为了可以让用户可以异步的获取到函数执行的返回值,我使用了 future 和 promise 特性,一部分代码如下:

    template <typename Fn, typename... Args, typename HasRet, typename Redundant>
    ::std::future<typename ::std::result_of<Fn(Args...)>::type>
    ThreadPool::submit(Fn&& func, Args... args)
    {
    	if (!running_)
    	{
    		LOGS_FATAL << "thread pool has not been initialized";
    	}
    	using RetType = typename ::std::result_of<Fn(Args...)>::type;
    	LockGuardType lock(mutex_);
    	auto promise = ::std::make_shared<::std::promise<RetType>>();
    	auto future = promise->get_future();
    	::std::function<RetType()> tmpTask = ::std::bind(::std::forward<Fn>(func), 					::std::forward<Args>(args)...);
    1 )	TaskType task = [t = ::std::move(tmpTask), promise]() mutable
    	{
    2 )		assert(promise.use_count() == 1);
    		promise->set_value(t());
    	};
    3 )    assert(promise.use_count() == 2);
        ......
        return future;
    }
    
    

    1 )处我用的 lambda 是值捕获 promise,3 )处的 use_count 总为 2 这没问题,现在的问题不知为什么 2 )处的 use_count 却不稳定,有时为 3 ,有时为 1 ? 下面为测试代码:

    TEST_F(ThreadPoolTest, SubmitHasRetval)
    {
    	::std::function<int32_t ()> f = []() -> int32_t
    	{
    		return 5;
    	};
    	auto future1 = threadPool->submit(f);
    	future1.wait();
    	ASSERT_EQ(future1.get(), 5);
    	auto future2 = threadPool->submit(::std::move(f));
    	future2.wait();
    	ASSERT_EQ(future2.get(), 5);
    }
    
    

    debug 模式下正常结果为:

    2022-05-10 11:09:44.942 [9924] DEBUG ThreadPool.cpp:111 - thread pool init success
    2022-05-10 11:09:44.943 [9924] DEBUG ThreadPool.cpp:128 - thread pool has been shutdown
    

    错误结果为:

    ... Assertion `promise.use_count() == 1' failed.
    2022-05-10 11:17:26.951 [10115] DEBUG ThreadPool.cpp:111 - thread pool init success
    

    感谢大佬们看完我这丑陋的代码。。。

    22 条回复    2022-05-11 01:07:11 +08:00
    guang19
        1
    guang19  
    OP
       2022-05-10 11:23:51 +08:00
    补充下:环境是 ubuntu wsl gcc 11.2.0 ,C++17
    iOCZ
        2
    iOCZ  
       2022-05-10 11:35:27 +08:00
    哎,C++真难看懂,就跟和尚的头皮点了香似的
    guang19
        3
    guang19  
    OP
       2022-05-10 11:52:29 +08:00
    我这样写也有问题:
    ````
    auto asyncTask = ::std::make_shared<::std::packaged_task<RetType ()>>(
    ::std::bind(::std::forward<Fn>(func), ::std::forward<Args>(args)...));
    auto future = asyncTask->get_future();
    TaskType task = [asyncTask]() mutable
    {
    assert(asyncTask.use_count() == 1);
    (*asyncTask)();
    };
    assert(asyncTask.use_count() == 2);
    ````

    我发现这应该是 lambda 捕获 shared_ptr 引用错乱的问题,可能不是我代码的问题
    Inn0Vat10n
        4
    Inn0Vat10n  
       2022-05-10 12:14:53 +08:00
    inline 掉了吧,可以看看汇编代码
    guang19
        5
    guang19  
    OP
       2022-05-10 12:40:34 +08:00
    又 debug 了下,大概是我阻塞队列写的有点问题。。。
    statumer
        6
    statumer  
       2022-05-10 12:50:06 +08:00
    除非你已经定位到问题了,否则建议把代码贴全。你的 shared_ptr 被 task 捕获以后,task 如果被拷贝的话,shared_ptr 也会一块儿被复制。
    elfive
        7
    elfive  
       2022-05-10 12:58:36 +08:00 via iPhone
    我记得好像是 std::thread 有问题,资源不会及时释放,好像是会一直有一份拷贝存在。
    后来我只好用 boost::thread 替换了,才解决这个问题。
    guang19
        8
    guang19  
    OP
       2022-05-10 13:05:30 +08:00
    @statumer 谢谢老哥,我大概知道为什么 use_count 为 3 了,在 shared_ptr 传参的过程中被复制了几次,所以造成引用次数不一致。有时候任务线程执行的快,被拷贝的 shared_ptr 来不及析构,lambda 此时执行的时候的 use_count 就是 3 ;任务线程执行的慢,等其他被拷贝的被析构了,只剩下 lambda 捕获的 shared_ptr 了,此时执行 use_count 就是 1 。我又测试了好多遍,虽然 use_count 不一致,但 task 执行的结果却没错,这证明了 lambda 只会是最后一个 shared_ptr ,lambda 结束后并不会造成泄漏。
    guang19
        9
    guang19  
    OP
       2022-05-10 13:09:52 +08:00
    @elfive 因为我写的这个库是基于 linux 的,所以自己基于 posix pthread 封装的线程,cpp11 的 thread 太难用了,跟坨屎一样,创建之后非要调 join 或 detach 才会执行,join 阻塞主线程就不说了,而 detach 更是难用。
    codefun666
        10
    codefun666  
       2022-05-10 15:19:02 +08:00
    c++太复杂了,用 c 吧。

    看到```::```就头疼...
    hhhWhy
        11
    hhhWhy  
       2022-05-10 15:37:52 +08:00
    @guang19 没有看代码,但是这里有点不对,cpp11 的 thread 是在完成初始化之前运行的
    hhhWhy
        12
    hhhWhy  
       2022-05-10 15:39:21 +08:00
    ColorfulBoar
        13
    ColorfulBoar  
       2022-05-10 15:41:28 +08:00
    @guang19 你这也太离谱了……谁教你的 join/detach 之后才会执行?真心建议重新学习一下标准库里面的 thread

    constructor:
    Effects: The new thread of execution executes...
    Synchronization: The completion of the invocation of the constructor synchronizes with the beginning
    of the invocation of the copy of f.

    void join();
    Effects: Blocks until the thread represented by *this has completed.
    Synchronization: The completion of the thread represented by *this synchronizes with (6.9.2) the
    corresponding successful join() return...

    void detach();
    Effects: The thread represented by *this continues execution without the calling thread blocking.
    When detach() returns, *this no longer represents the possibly continuing thread of execution...
    guang19
        14
    guang19  
    OP
       2022-05-10 15:50:11 +08:00
    @ColorfulBoar 那请你帮个忙把这段代码贴到 linux 下去运行下,这个线程会不会执行,我的 archlinux 和 ubuntu 反正是不行的:
    ````
    ::std::thread t1([] ()
    {
    ::printf("%s\n", "hello world");
    });
    ::std::this_thread::sleep_for(::std::chrono::milliseconds(5000));
    ````
    guang19
        15
    guang19  
    OP
       2022-05-10 16:04:04 +08:00
    @ColorfulBoar 等 5 秒中都不执行的线程,非要手工指定 join 或 detach 状态的线程,你觉得离谱吗?
    ColorfulBoar
        16
    ColorfulBoar  
       2022-05-10 16:12:46 +08:00
    @guang19 这……你不会以为 printf 是直接写屏幕上的所以不显示等于没执行吧?你这个在当前 scope 结束的时候 t1 仍然是 joinable 的,所以 destructor 会调用 std::terminate(),然后 stdout 的缓冲区里面的东西就直接被扔了所以看起来什么都没有。你关了 buffer 或者手动刷新一下就能看出来了。
    guang19
        17
    guang19  
    OP
       2022-05-10 16:25:47 +08:00
    @ColorfulBoar 惊了,学习了,谢谢大佬。
    wzzzx
        18
    wzzzx  
       2022-05-10 21:17:19 +08:00
    @ColorfulBoar #16 我有个疑问想请教一下,我知道在当前 scope 结束后,t1 仍然是 joinable 的,但这是为什么丫?为什么要这么设计?明明已经执行完毕了,为什么还需要额外去调用一下 join 来保证它不是 joinable 的呢?
    wzzzx
        19
    wzzzx  
       2022-05-10 21:18:22 +08:00
    @ColorfulBoar #16 https://en.cppreference.com/w/cpp/thread/thread/joinable
    文档里也是这么说的
    ```
    A thread that has finished executing code, but has not yet been joined is still considered an active thread of execution and is therefore joinable.
    ```
    但是我不大理解这么设计的原因
    kilasuelika
        20
    kilasuelika  
       2022-05-10 21:48:07 +08:00 via Android   ❤️ 2
    你的 std 前为啥要用::?
    nightwitch
        21
    nightwitch  
       2022-05-11 00:01:21 +08:00   ❤️ 1
    在多线程环境下不要用 shared_ptr 的 use_count()以及 uniqiue() (已经在 C++20 被删除)这个 API 。
    cppreference 上明确标记了这两个 API 不保证线程安全,所以在多线程环境下其只返回一个可能的值,不是精确的。

    reference:https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count

    @wzzzx 好问题,除了不会自动 join 以外,std::thread 也不能被打断或者取消,这个问题在 C++20 得到了修复。标准库加入了 std::jthread ,见 https://en.cppreference.com/w/cpp/thread/jthread
    ColorfulBoar
        22
    ColorfulBoar  
       2022-05-11 01:07:11 +08:00
    @wzzzx joinable()只是检测这个 std::thread object 是不是依然持有一个进程而已别的啥都没干,或许你想问的是为什么 std::thread 的 destructor 这么奇怪……当然不管想问的是啥,不管把标准还是实现重复一遍肯定都没意思,你一定想听个刺激的,所以让我恶意转述一下史官 Bjarne Stroustrup 的说法:我们当年也不想这样,但活在 [世界上只有 C 语言 + POSIX 的 P 意思是 portable] 这个梦里的 C 标准委员会因为 C 里面没有 RAII / POSIX 天生残疾就以为别人也不行,强烈反对我们干人事(这是唯一一份 C 标准委员会发给 C++标准委员会的正式通知),那就别怪我们摆烂了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   876 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 20:34 · PVG 04:34 · LAX 12:34 · JFK 15:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.