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

本文主要是介绍std::bind中传入的实参变量的生命周期不能短于生成的可调用对象的生命周期,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

错误示例

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

#include <memory>
#include <functional>
#include <string>
#include <iostream>
#include <thread>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 <memory>
#include <functional>
#include <string>
#include <iostream>
#include <thread>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 <memory>
#include <functional>
#include <string>
#include <iostream>
#include <thread>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();
}

这篇关于std::bind中传入的实参变量的生命周期不能短于生成的可调用对象的生命周期的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/640065

相关文章

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

使用Java读取本地文件并转换为MultipartFile对象的方法

《使用Java读取本地文件并转换为MultipartFile对象的方法》在许多JavaWeb应用中,我们经常会遇到将本地文件上传至服务器或其他系统的需求,在这种场景下,MultipartFile对象非... 目录1. 基本需求2. 自定义 MultipartFile 类3. 实现代码4. 代码解析5. 自定

Java调用Python脚本实现HelloWorld的示例详解

《Java调用Python脚本实现HelloWorld的示例详解》作为程序员,我们经常会遇到需要在Java项目中调用Python脚本的场景,下面我们来看看如何从基础到进阶,一步步实现Java与Pyth... 目录一、环境准备二、基础调用:使用 Runtime.exec()2.1 实现步骤2.2 代码解析三、

C#使用Spire.XLS快速生成多表格Excel文件

《C#使用Spire.XLS快速生成多表格Excel文件》在日常开发中,我们经常需要将业务数据导出为结构清晰的Excel文件,本文将手把手教你使用Spire.XLS这个强大的.NET组件,只需几行C#... 目录一、Spire.XLS核心优势清单1.1 性能碾压:从3秒到0.5秒的质变1.2 批量操作的优雅

Python使用python-pptx自动化操作和生成PPT

《Python使用python-pptx自动化操作和生成PPT》这篇文章主要为大家详细介绍了如何使用python-pptx库实现PPT自动化,并提供实用的代码示例和应用场景,感兴趣的小伙伴可以跟随小编... 目录使用python-pptx操作PPT文档安装python-pptx基础概念创建新的PPT文档查看

Python如何调用另一个类的方法和属性

《Python如何调用另一个类的方法和属性》在Python面向对象编程中,类与类之间的交互是非常常见的场景,本文将详细介绍在Python中一个类如何调用另一个类的方法和属性,大家可以根据需要进行选择... 目录一、前言二、基本调用方式通过实例化调用通过类继承调用三、高级调用方式通过组合方式调用通过类方法/静

C#控制台程序同步调用WebApi实现方式

《C#控制台程序同步调用WebApi实现方式》控制台程序作为Job时,需同步调用WebApi以确保获取返回结果后执行后续操作,否则会引发TaskCanceledException异常,同步处理可避免异... 目录同步调用WebApi方法Cls001类里面的写法总结控制台程序一般当作Job使用,有时候需要控制

在ASP.NET项目中如何使用C#生成二维码

《在ASP.NET项目中如何使用C#生成二维码》二维码(QRCode)已广泛应用于网址分享,支付链接等场景,本文将以ASP.NET为示例,演示如何实现输入文本/URL,生成二维码,在线显示与下载的完整... 目录创建前端页面(Index.cshtml)后端二维码生成逻辑(Index.cshtml.cs)总结

Python实现数据可视化图表生成(适合新手入门)

《Python实现数据可视化图表生成(适合新手入门)》在数据科学和数据分析的新时代,高效、直观的数据可视化工具显得尤为重要,下面:本文主要介绍Python实现数据可视化图表生成的相关资料,文中通过... 目录前言为什么需要数据可视化准备工作基本图表绘制折线图柱状图散点图使用Seaborn创建高级图表箱线图热

Python用Flask封装API及调用详解

《Python用Flask封装API及调用详解》本文介绍Flask的优势(轻量、灵活、易扩展),对比GET/POST表单/JSON请求方式,涵盖错误处理、开发建议及生产环境部署注意事项... 目录一、Flask的优势一、基础设置二、GET请求方式服务端代码客户端调用三、POST表单方式服务端代码客户端调用四