今天复习前几年在项目过程中积累的各类技术案例,有一个小的 coredump 案例,当时小组里几位较资深的同事都没看出来,后面是我周末查了两三个小时解决掉的,今天再做一次系统的总结,给出一个复现的案例代码,案例代码比较简单,便于学习理解。

1. 简介

原则:临时对象不应该被 lambda 引用捕获,因为临时对象在它所在的语句结束就会被析构掉,只能采用值捕获。

当临时对象比较隐蔽时,我们就可能犯这个低级错误。本文介绍一类case:以基类智能指针对象的 const 引用为函数形参,并在函数内对该参数做引用捕获,然后进行跨线程异步使用。当函数调用者使用派生类智能指针作为实参时,此时派生类智能指针对象会向上转换为基类智能指针对象,这个转换是隐式的,产生的对象是临时对象,然后被 lambda 引用捕获,后续跨线程使用引发“野引用” core。

2. 案例

下面写一个简单的 demo 代码来模拟这个案例。案例涉及的代码流程,如下图所示:



其中,基类 BaseTask,派生类 DerivedTask,main 函数将 lambda 闭包抛到工作线程中异步执行。

详细示例代码如下:

/**
* @brief 关键字:lambda、多线程、std::shared_ptr 隐式向上转换
* g++ main.cc -std=c++17 -O3 -lpthread
*/ #include <atomic>
#include <chrono>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <thread> using namespace std::chrono_literals; /// 简易线程池
template <typename Func>
class ThreadPool {
public:
~ThreadPool() {
stop_ = true;
for (auto& item : workers_) {
item.join();
}
} void Run() {
static constexpr uint32_t kThreadNum = 2; uint32_t idx = 0;
for (uint32_t idx = 0; idx != kThreadNum; ++idx) {
workers_.emplace_back(std::thread([this, idx] { ThreadFunc(idx); }));
mutexs_.emplace_back(std::make_shared<std::mutex>());
} job_queues_.resize(kThreadNum);
} void Stop() { stop_ = true; } bool Post(Func&& f) {
if (!stop_) {
uint32_t index = ++job_cnt_ % job_queues_.size();
auto& queue = job_queues_[index];
std::lock_guard<std::mutex> locker(*mutexs_[index]);
queue.push(std::move(f));
return true;
} return false;
} void ThreadFunc(uint32_t idx) {
auto& queue = job_queues_[idx];
auto& mutex = *mutexs_[idx];
// 退出前清空任务队列
while (true) {
if (!queue.empty()) {
std::lock_guard<std::mutex> locker(mutex);
const auto& job_func = queue.front();
job_func();
queue.pop();
} else if (!stop_) {
std::this_thread::sleep_for(10ms);
} else {
break;
}
}
} private:
/// 工作线程池
std::vector<std::thread> workers_;
/// 任务队列,每个工作线程一个队列
std::vector<std::queue<Func>> job_queues_;
/// 任务队列的读写保护锁,每个工作线程一个锁
std::vector<std::shared_ptr<std::mutex>> mutexs_;
/// 是否停止工作
bool stop_ = false;
/// 任务计数,用于将任务均衡分配给多线程队列
std::atomic<uint32_t> job_cnt_ = 0;
};
using MyThreadPool = ThreadPool<std::function<void()>>; /// 基类task
class BaseTask {
public:
virtual ~BaseTask() = default;
virtual void DoSomething() = 0;
};
using BaseTaskPtr = std::shared_ptr<BaseTask>; /// 派生task
class DeriveTask : public BaseTask {
public:
void DoSomething() override {
std::cout << "derive task do someting" << std::endl;
}
};
using DeriveTaskPtr = std::shared_ptr<DeriveTask>; /// 示例用户
class User {
public:
User() { thread_pool_.Run(); }
~User() { thread_pool_.Stop(); } void DoJobAsync(const BaseTaskPtr& task) {
// task 是 user->DoJob 调用产生的临时对象,捕获它的引用会变成也指针
thread_pool_.Post([&task] { task->DoSomething(); });
} private:
MyThreadPool thread_pool_;
}; using UserPtr = std::shared_ptr<User>; /// 测试运行出 core
int main() {
auto user = std::make_shared<User>();
DeriveTaskPtr derive_task1 = std::make_shared<DeriveTask>();
// derive_task 会隐式转换为 BaseTask 智能指针对象,
// 该对象是临时对象,在 DoJob 执行完之后生命周期结束。
user->DoJobAsync(derive_task1); DeriveTaskPtr derive_task3 = std::make_shared<DeriveTask>();
user->DoJobAsync(derive_task3); std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}

上面这个例子代码,会出现 coredump,或者是没有执行派生类的 DoSomething,总之是不符合预期。不符合预期的原因如下:这份代码往一个线程里 post lambda 函数,lambda 函数引用捕获智能指针对象,这是一个临时对象,其离开使用域之后会被析构掉,导致 lambda 函数在异步线程执行时,访问到一个"野引用"出错。而之所以捕获的智能指针是临时对象,是因为调用 User.DoJobAsync 时发生了类型的向上转换。

上述的例子还比较容易看出来问题点,但当我们的项目代码层次较深时,这类错误就非常难看出来,也因此之前团队里的资深同事也都无法发现问题所在。

这类问题有多种解决办法:

(1)方法1:避免出现隐式转换,消除临时对象;

(2)方法2:函数和 lambda 捕获都修改为裸指针,消除临时对象;引用本质上是指针,需要关注生命周期,既然采用引用参数就表示调用者需要保障对象的生命周期,智能指针的引用在用法上跟指针无异,那么这里不如用裸指针,让调用者更清楚自己需要保障对象的生命周期;

(3)方法3:异步执行时采用值捕获/值传递,不采用引用捕获,但值捕获可能导致性能浪费,具体到本文的例子,这里的性能开销是一个智能指针对象的构造,性能损耗不大,是可接受的。

3. 其他

临时对象的生命周期可以参考这篇文档:https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary

C++ lambda 引用捕获临时对象引发 core 的案例的更多相关文章

  1. C++ —— 非常量引用不能指向临时对象

    目录 举例 分析 解决 1.举例 非常量引用 指向 临时对象 —— 即:将 临时对象 传递给 非常量引用类型. 如以下情况就会出现: 实现实数Rational类,实数可以使用+号相加,运算的结果要可以 ...

  2. 【C++】C++中的lambda表达式和函数对象

    目录结构: contents structure [-] lambda表达式 lambda c++14新特性 lambda捕捉表达式 泛型lambda表达式 函数对象 函数适配器 绑定器(binder ...

  3. 【编程篇】C++11系列之——临时对象分析

    /*C++中返回一个对象时的实现及传说中的右值——临时对象*/ 如下代码: /**********************************************/ class CStuden ...

  4. C++临时对象的生命期

    class Test{ public: Test(int a):m_int(a){ printf("this is Test(%d) ctor\n", m_int); } ~Tes ...

  5. SQL Server 内置函数、临时对象、流程控制

    SQL Server 内置函数 日期时间函数 --返回当前系统日期时间 select getdate() as [datetime],sysdatetime() as [datetime2] getd ...

  6. C++中临时对象的学习笔记

    http://www.cppblog.com/besterChen/category/9573.html 所属分类: C/C++/STL/boost  在函数调用的时候,无论是参数为对象还是返回一个对 ...

  7. C++ 临时对象

    1.什么是临时对象? swap方法中,常常定义一个temp对象,这个temp对象不是临时对象,而是局部对象.这里所说的临时对象是不可见的,在原代码中是看不到的. 2.为什么会产生临时对象? a.客户期 ...

  8. 【M19】了解临时对象的来源

    1.首先,确认什么是临时对象.在swap方法中,建立一个对象temp,程序员往往把temp称为临时对象.实际上,temp是个局部对象.C++中所谓的临时对象是不可见的,产生一个non-heap对象,并 ...

  9. [012]泛型--lambda表达式捕获

    lambda表达式的捕获跟参数差不多,可以是值或者引用. 1.值捕获 与传值参数类似,采用值捕获的前期是变量可以拷贝:与参数不通透的是:被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝. ...

  10. STL——临时对象的产生与运用

    所谓临时对象,就是一种无名对象.它的出现如果不在程序员的预期之下(例如任何pass by value操作都会引发copy操作,于是形成一个临时对象),往往造成效率上的负担.但有时候刻意制造一些临时对象 ...

随机推荐

  1. CentOS7源码安装Python3

    一.先安装python3所依赖的软件包,非常重要(否则可能会出现python3安装成功,却缺少相应的pip) [root@centos src]# yum groupinstall "Dev ...

  2. ubuntu16.04个性化配置

    前言 记录一下个人配置,方便后续参考 正文 配置用户sudo免密权限 只建议在个人测试环境这么配置,否则最好root还是需要用密码确认一下 sudo su echo "你的用户名 ALL=( ...

  3. documen.write 和 innerHTML 的区别?

    document.write只能重绘整个页面,innerHTML可以重绘页面的一部分. 1. ducument.write使用举例html文档: <!doctype html> <h ...

  4. SpringBoot目录文件结构和官方推荐的目录规范、静态资源访问

    目录讲解 src/main/java:存放代码 src/main/resourcces static:存放静态文件,比如css.js.image,(访问方式:http://localhost:8080 ...

  5. JDK1.8新特性Lambda表达式简化if-else里都有for循环的优化方式

    在日常开发过程当中,能把代码写出来,不一定就意味着能把代码写好,说不准,所写的代码在他人看来,其实就是一坨乱七八糟的翔,因此,代码简化尤其重要,我曾经遇到过这样一个类型的代码,即if-else里都有相 ...

  6. 洛谷P1439

    这道题也给了我很多的思考,因为很久没有做过LIS和KLCS的题了 为什么能采用二分 因为f数组保存的是LCS长度为i时的最小末尾的值,可以证明f数组一定是单调的,并且是严格单调的 为什么要保存末尾最小 ...

  7. Swift开发基础05-可选项

    可选项定义 可选项,一般也叫可选类型,它允许将值设置为nil 在类型名称后面加个问号? 来定义一个可选项 var name: String? = "Jack" name = nil ...

  8. [oeasy]python0030_设置路径_export_PATH_zsh_系统路径设置_export

    ​ 放入路径 回忆上次内容 我们要在任意路径下直接执行 sleep.py 把 sleep.py 放在 /usr/bin/ 下面 最终可以在任意位置执行程序sleep.py 但是 /usr/bin 里面 ...

  9. C# 对象复制三种方法效率对比——反射、序列化、表达式树

    1.需求 在代码中经常会遇到需要把对象复制一遍,或者把属性名相同的值复制一遍. 比如: public class Student { public int Id { get; set; } publi ...

  10. vue codemirro 文件对比 sql编辑器

    效果图: 安装及引用同上一篇文章,请移步:https://www.cnblogs.com/Lu-Lu/p/16265815.html HTML: <el-dialog class="d ...