std::bind中传入的实参变量的生命周期不能短于生成的可调用对象的生命周期

在使用bind生成可调用对象时,bind的中传入的实参变量的生命周期不能短于生成的可调用对象的生命周期。

错误示例

一个错误示例:给 bind 传递的参数为引用类型,然而该引用变量的生命周期短于生成的可调用对象的生命周期,从而导致了在调用 bind 生成的可调用对象时,该引用变量变成了悬垂引用。

#include 
#include 
#include 
#include 
#include 

using Func = std::function<void()>;
void queueInLoop(Func);

void runInLoop(Func func)
{
    queueInLoop(std::move(func));
}

void queueInLoop(Func func)
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    func();
}

void insertInLoop(std::unique_ptr<std::string> &str)
{
    std::cout << "insertInLoop" << std::endl;
    std::cout << *str << std::endl;
}

void func(std::unique_ptr<std::string>& pstr)
{
    runInLoop(std::move(std::bind(&insertInLoop, std::ref(pstr))));
}

int main()
{
    std::unique_ptr<std::string> str(new std::string("hello"));
    std::thread t(func, std::ref(str));
    str.reset();  // 令其管理的对象销毁
    t.join();
}

示例说明:

  • main 函数中,创建了一个线程,将 main 中的 str 变量传递给引用传递给创建的线程。
  • 调用 std::thread 创建并启动一个线程,然后调用了 str.reset() ,模拟 str 所管理对象的销毁,模拟出悬垂引用。
  • 在新创建的线程中(线程主函数 func),使用 std::bindinsertInLoop 与 传入funcpstr 进行绑定,这里仍然是引用传递。然后将新生成的可调用对象传入给 runInLoop 函数,这里的参数传递方式是值移动。
  • runInLoop 函数中进行函数嵌套调用(至于这里为什么要进行函数嵌套调用,简单解释一下。这个 demo 来自muduo网络库,是我在重写过程中遇到的一个 bug。在这个 demo 中,只需重点关注变量生命周期问题导致悬垂引用)。在 queueInLoop 函数中,令其睡眠3s,模拟延长 std::bind 生成的可调用对象的调用,从而模拟出悬垂引用的现象。
  • queueInLoop 函数中传入的可调用对象被执行,传入的可调用对象即 func 中的 std::bind(&insertInLoop, std::ref(pstr))func 的调用等价于 insertInLoop(str)strmain 函数中传入的 变量。然而此时 main 中的 str 已销毁了其管理的对象(str.reset()),func 可调用对象中执行的 std::cout << *str << std::endl; 语句中,*str 访问了一个悬垂引用,导致程序出错。

下面的示例对上述程序添加了打印输出,可以运行查看悬垂引出产生的时机。

#include 
#include 
#include 
#include 
#include 

using Func = std::function<void()>;
void queueInLoop(Func);

void runInLoop(Func func)
{
    queueInLoop(std::move(func));
}

void queueInLoop(Func func)
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    func();
}

void insertInLoop(std::unique_ptr<std::string> &str)
{
    std::cout << "insertInLoop" << std::endl;
    std::cout << *str << std::endl;
}

void insertInLoop2(std::string &str)
{
    std::cout << str << std::endl;
}

void func(std::unique_ptr<std::string>& pstr)
{
    std::this_thread::sleep_for(std::chrono::seconds(2));
    if (pstr) {
        std::cout << "pstr is valid." << std::endl;
    }
    else {
        std::cout << "pstr is invalid!" << std::endl;
        std::cout << "cout *pstr will be segmentation fault!" << std::endl;
        std::cout << *pstr << std::endl;
    }
    std::cout << "before runInLoop" << std::endl;
    runInLoop(std::move(std::bind(&insertInLoop, std::ref(pstr))));
}

int main()
{
    std::unique_ptr<std::string> str(new std::string("hello"));
    std::thread t(func, std::ref(str));
    str.reset();
    t.join();
}

解决办法

在给出上述问题的解决方法之前,先把可能会出现上述情况的场景总结如下:

我们需要在线程A中在堆上申请一块内存资源,并且可能会传递给线程B使用,并将其生命周期交给线程B管理,且我们希望使用 unique_ptr 来替换原始指针管理内存资源。对着跨线程使用的场景,如上面的示例所示,我们需要使用 bind 将这块动态内存进行绑定以生成一个可调用对象,然后传递给另一个线程调用,这就可能出现上述示例中的悬垂引用的情况了。

我给出的一种解决思路是,在跨线程调用时,比如在线程A中,使用原始指针进行创建,然后使用原始指针以值拷贝的形式跨线程传递给B,在B线程中在使用 unique_ptr 接管这块内存,从而避免线程A中 unique_ptr 管理的内存提前释放的问题。修改后的代码示例如下:

#include 
#include 
#include 
#include 
#include 

using Func = std::function<void()>;
void queueInLoop(Func);

void runInLoop(Func func)
{
    queueInLoop(std::move(func));
}

void queueInLoop(Func func)
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    func();
}

void insertInLoop(std::string *str)
{
    // 假设 insertInLoop 是在线程B中被调用
    // 在线程中使用 unique_ptr 接管线程A传入的原始指针
    std::unique_ptr<std::string> pstr(str);
    std::cout << "insertInLoop" << std::endl;
    std::cout << *pstr << std::endl;
}

void func(std::string* pstr)
{
    std::this_thread::sleep_for(std::chrono::seconds(2));
    runInLoop(std::bind(&insertInLoop, pstr));
}

// 线程A
int main()
{
    std::string* str = new std::string("hello");
    std::thread t(func, str);
    t.join();
}

你可能感兴趣的:(cpp,c++,开发语言)