[c++] 记录一次引用使用不当导致的 bug
在工作中看到了如下代码,代码基于 std::thread 封装了一个 Thread 类。Thread 封装了业务开发中常用的接口,比如设置调度策略,设置优先级,设置线程名。如下代码删去了不必要的代码,只保留能说明问题的代码。从代码实现上来看,我们看不出什么问题,创建一个线程,第一个形参是线程的入口函数,后边的传参是线程入口函数的参数列表。
class Thread {
public:template <class Function, class... Args>Thread(Function &&f, Args &&...args) noexcept: internal_{[func = std::forward<Function>(f), &args...]() {func(args...);}}{}private:std::thread internal_;
};
Thread 类在大部分使用场景下是没问题的,比如下面的使用方式,创建了一个线程,线程中是一个死循环,每隔一秒打印一次 "thread running",可以正常工作。
#include <stdio.h>
#include <unistd.h>
#include <memory>
#include <thread>
#include <vector>class Thread {
public:template <class Function, class... Args>Thread(Function &&f, Args &&...args) noexcept: internal_{[func = std::forward<Function>(f), args...]() {func(args...);}}{}private:std::thread internal_;
};void func() {while (1) {printf("thread running\n");sleep(1);}
}int main() {Thread *t = new Thread(func);sleep(100);return 0;
}
1 问题现象
在下边这个使用场景下,就能暴露出来 Thread 的问题。
如下代码中连续创建了 8 个线程,线程的入口函数是 func(),func() 的形参是 Obj 对象,Obj 中的成员 i_ 分别取值 0 ~ 7。
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>class Thread {
public:template <class Function, class... Args>Thread(Function &&f, Args &&...args) noexcept: internal_{[func = std::forward<Function>(f), &args...]() {func(args...);}}{}private:std::thread internal_;
};class Obj {
public:Obj(int i) {i_ = i;std::cout << "Obj(), i: " << i_ << std::endl;}Obj(const Obj &obj) {i_ = obj.i_;std::cout << "copy constructor, i: " << i_ << std::endl;}~Obj() {std::cout << "~Obj(), i: " << i_ << std::endl;}int i_;};void func(Obj obj) {printf(" in thread, i: %d\n", obj.i_);
}int main() {std::vector<Thread *> threads;int i = 0;for (i = 0; i < 8; i++) {printf(" out thread, i: %d\n", i);Obj obj(i);auto tmp = new Thread(func, obj);printf("after create thread %d\n", i);threads.emplace_back(tmp);// sleep(2);}sleep(100);return 0;
}
上边的代码编译之后,运行结果如下所示。我们的预期是在 func() 中的打印分别是 0 ~ 7,每个数字打印一次。但实际的打印结果是有重复的,如下图所示,2 有重复的,7 也有重复的。
root@wangyanlong-virtual-machine:/home/wyl/cpp# ./a.outout thread, i: 0
Obj(), i: 0
after create thread 0
~Obj(), i: 0out thread, i: 1
Obj(), i: 1
after create thread 1
~Obj(), i: 1out thread, i: 2
Obj(), i: 2
copy constructor, i: 2in thread, i: 2
~Obj(), i: 2
copy constructor, i: 2in thread, i: 2
~Obj(), i: 2
after create thread 2
~Obj(), i: 2out thread, i: 3
Obj(), i: 3
copy constructor, i: 2in thread, i: 2
~Obj(), i: 2
copy constructor, i: 3in thread, i: 3
~Obj(), i: 3
after create thread 3
~Obj(), i: 3out thread, i: 4
Obj(), i: 4
after create thread 4
~Obj(), i: 4out thread, i: 5
Obj(), i: 5
copy constructor, i: 4
after create thread 5
~Obj(), i: 5out thread, i: 6
Obj(), i: 6in thread, i: 4
~Obj(), i: 4
copy constructor, i: 5in thread, i: 5
~Obj(), i: 5
after create thread 6
~Obj(), i: 6out thread, i: 7
Obj(), i: 7
copy constructor, i: 7in thread, i: 7
~Obj(), i: 7
after create thread 7
~Obj(), i: 7
copy constructor, i: 7in thread, i: 7
~Obj(), i: 7
上边的代码把 sleep(2) 注释打开,打印结果是符合预期的。
或者将 main() 中的 Thread() 改成 std::thread,打印结果也是符合预期的,说明这种使用方式是符合 c++ 规范的。
2 问题分析
导致问题的原因有以下几个方面:
(1)线程的构造函数入参是右值引用,这个右值引用的生命周期在构造函数返回的时候已经结束了。右值引用,指向一个临时的存储空间,在反复创建 8 个线程期间,8 个右值引用指向的是同一块内存空间,后边的值会将前边的值覆盖。
(2)线程构造函数中,std::thread 的回调函数是一个 lambda 表达式,lambda 表达式中引用捕获了 args。
(3)在 Thread 构造函数中创建了线程,但是线程并不是立即执行的,从创建到真正执行是有一段时间的延迟。这样当线程真正运行的时候,再从 args 引用里边读取数据,取出来的是这块内存最新的数据,属于这个线程的数据已经被覆盖。
3 问题修改
引用捕获改成值捕获
如下代码,在 Thread() 构造函数中的 lambda 表达式对 args 的引用捕获改成值捕获。
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>class Thread {
public:template <class Function, class... Args>Thread(Function &&f, Args &&...args) noexcept: internal_{[func = std::forward<Function>(f), args...]() {func(args...);}}{}private:std::thread internal_;
};class Obj {
public:Obj(int i) {i_ = i;std::cout << "Obj(), i: " << i_ << std::endl;}Obj(const Obj &obj) {i_ = obj.i_;std::cout << "copy constructor, i: " << i_ << std::endl;}~Obj() {std::cout << "~Obj(), i: " << i_ << std::endl;}int i_;};void func(Obj obj) {printf(" in thread, i: %d\n", obj.i_);
}int main() {std::vector<Thread *> threads;int i = 0;for (i = 0; i < 8; i++) {printf(" out thread, i: %d\n", i);Obj obj(i);auto tmp = new Thread(func, obj);printf("after create thread %d\n", i);threads.emplace_back(tmp);// sleep(2);}sleep(100);return 0;
}
相关文章:
[c++] 记录一次引用使用不当导致的 bug
在工作中看到了如下代码,代码基于 std::thread 封装了一个 Thread 类。Thread 封装了业务开发中常用的接口,比如设置调度策略,设置优先级,设置线程名。如下代码删去了不必要的代码,只保留能说明问题的代码。从代码实现…...
能不能节约百分之九十的算力来训练模型
Sora是由OpenAI开发的视频生成模型,它采用了多种先进的技术和架构,能够根据文本描述生成长达一分钟的高清视频。虽然OpenAI并未公开Sora的详细模型架构和实现细节,但我们可以根据公开的信息和参考论文来了解其技术架构。 Sora的核心技术架构主…...
LeetCode206: 反转链表.
题目描述 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例 解题方法 假设链表为 1→2→3→∅,我们想要把它改成∅←1←2←3。在遍历链表时,将当前节点的 next指针改为指向前一个节点。由于节点没有引用其前一…...
高级统计方法 第1次作业
概念 1. 请解释什么是P值,怎么计算p值,p值结果怎么理解,p值有哪些应用......? (a)什么是P值 P值是一种用来判定假设检验结果的一个参数,它描述了在原假设为真的情况下,比所得到的…...
spinalhdl,vivado,fpga
https://spinalhdl.github.io/SpinalDoc-RTD/master spinal hdl sudo apt install openjdk-17-jdk scala curl echo “deb https://repo.scala-sbt.org/scalasbt/debian all main” | sudo tee /etc/apt/sources.list.d/sbt.list echo “deb https://repo.scala-sbt.org/scal…...
Tomcat线程池原理(下篇:工作原理)
文章目录 前言正文一、执行线程的基本流程1.1 JUC中的线程池执行线程1.2 Tomcat 中线程池执行线程 二、被改造的阻塞队列2.1 TaskQueue的 offer(...)2.2 TaskQueue的 force(...) 三、总结 前言 Tomcat 线程池,是依据 JUC 中的线程池 ThreadPoolExecutor 重新自定义…...
【服务器数据恢复】通过reed-solomon算法恢复raid6数据的案例
服务器数据恢复环境: 一台网站服务器中有一组由6块磁盘组建的RAID6磁盘阵列,操作系统层面运行MySQL数据库和存放一些其他类型文件。 服务器故障: 该服务器在工作过程中,raid6磁盘阵列中有两块磁盘先后离线,不知道是管理…...
LeetCode 2583.二叉树中的第 K 大层和:层序遍历 + 排序
【LetMeFly】2583.二叉树中的第 K 大层和:层序遍历 排序 力扣题目链接:https://leetcode.cn/problems/kth-largest-sum-in-a-binary-tree/ 给你一棵二叉树的根节点 root 和一个正整数 k 。 树中的 层和 是指 同一层 上节点值的总和。 返回树中第 k …...
element ui 安装 简易过程 已解决
我之所以将Element归类为Vue.js,其主要原因是Element是(饿了么团队)基于MVVM框架Vue开源出来的一套前端ui组件。我最爱的就是它的布局容器!!! 下面进入正题: 1、Element的安装 首先你需要创建…...
websoket
WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。你可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应,比较典型的应用场景就是即时通讯(聊天)系统。 <!DOC…...
案例:微服务从Java/SpringBoot迁移到Golan
基于 Java 的微服务,特别是那些使用 Spring Boot 的微服务,长期以来因其强大的功能和广泛的社区支持而闻名。Spring Boot 的约定优于配置方法简化了微服务的部署和开发,提供了大量开箱即用的功能,例如自动配置、独立功能和简单的依…...
小波变换模拟
小波变换是一种信号处理技术,通过在时间-频率域中使用基于小波的函数进行信号分析。小波变换在处理非平稳信号和图像时特别有用,可以将信号分解为不同频率的成分。它在数据压缩、去噪、特征提取等领域有广泛应用。 MATLAB中提供了用于二维离散小波变换的…...
cv::Mat图像操作
图像读写 //include header #include <opencv2/imgcodecs.hpp>/** Currently, the following file formats are supported: Windows bitmaps - *.bmp, *.dib (always supported) JPEG files - *.jpeg, *.jpg, *.jpe (see the Note section) JPEG 2000 files - *.jp2 (s…...
【机器学习基础】一元线性回归(适合初学者的保姆级文章)
🚀个人主页:为梦而生~ 关注我一起学习吧! 💡专栏:机器学习 欢迎订阅!后面的内容会越来越有意思~ 💡往期推荐: 【机器学习基础】机器学习入门(1) 【机器学习基…...
2024年软件测试岗位-面试
第一部分: 1、自我介绍:简历写到的快速描述,学校、学历、工作经验等(注意:不要过度优化简历,你不写别人可能会问,但你写了别人一定会问!) 第二部分: 1、功能测…...
【坑】Spring Boot整合MyBatis,一级缓存失效
一、Spring Boot整合MyBatis,一级缓存失效 1.1、概述 MyBatis一级缓存的作用域是同一个SqlSession,在同一个SqlSession中执行两次相同的查询,第一次执行完毕后,Mybatis会将查询到的数据缓存起来(缓存到内存中…...
J7 - 对于ResNeXt-50算法的思考
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 | 接辅导、项目定制 J6周有一段代码如下 思考过程 首先看到这个问题的描述,想到的是可能使用了向量操作的广播机制然后就想想办法验证一下&…...
R3F(React Three Fiber)基础篇
之前一直在做ThreeJS方向,整理了两篇R3F(React Three Fiber)的文档,这是基础篇,如果您的业务场景需要使用R3F,您又对R3F不太了解,或者不想使用R3F全英文文档,您可以参考一下这篇&…...
torch\tensorflow在大语言模型LLM中的作用
文章目录 torch\tensorflow在大语言模型LLM中的作用 torch\tensorflow在大语言模型LLM中的作用 在大型语言模型(LLM)中,PyTorch和TensorFlow这两个深度学习框架起着至关重要的作用。它们为构建、训练和部署LLM提供了必要的工具和基础设施。 …...
设计模式-创建型模式-单例模式
0 引言 创建型模式(Creational Pattern)关注对象的创建过程,是一类最常用的设计模式,每个创建型模式都通过采用不同的解决方案来回答3个问题:创建什么(What),由谁创建(W…...
OpCore-Simplify:黑苹果配置的自动化革命——从复杂调试到一键配置的智能解决方案
OpCore-Simplify:黑苹果配置的自动化革命——从复杂调试到一键配置的智能解决方案 【免费下载链接】OpCore-Simplify A tool designed to simplify the creation of OpenCore EFI 项目地址: https://gitcode.com/GitHub_Trending/op/OpCore-Simplify 传统黑苹…...
打字侠全面支持三大五笔输入法:初学者快速上手指南
1. 五笔输入法:为什么值得初学者投入时间? 在拼音输入法大行其道的今天,很多初学者可能会疑惑:为什么要花时间学习看起来更复杂的五笔输入法?其实答案很简单——效率。我十年前刚开始接触五笔时也有同样的困惑…...
立创·地阔星开发板开箱测评:除了点灯,STM32F103C8T6还能怎么玩?(附资源下载与避坑指南)
立创地阔星开发板深度探索:从开箱到创意项目实战 拆开快递包装的那一刻,这块蓝色PCB板安静地躺在防静电袋里——这就是最近在创客圈备受关注的立创地阔星开发板。作为一款基于STM32F103C8T6芯片的高性价比开发平台,它不仅适合初学者入门&…...
电源管理入门-4子系统reset
之前的文章电源管理入门-1关机重启详解介绍了整机SoC的重启也可以说是reset,那么子系统的reset,例如某个驱动(网卡、USB等)或者某个子系统(NPU、ISP等运行在独立的M核或者R核上的AI系统),这些零…...
原创:光刻机中下游质量约束框架:从底层落地破局芯片制造困局
光刻机中下游质量约束框架:从底层落地破局芯片制造困局 作者:华夏之光永存 摘要 当下国内芯片产业陷入一个普遍误区:将攻克EUV光刻机整机视为破局“卡脖子”的唯一核心,大量资源集中投入上游光刻机研发,却严重忽视中下…...
告别答辩 PPT 熬夜局!PaperXie AI 一键生成,3 分钟拿捏学术范答辩神器
paperxie-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/AIPPThttps://www.paperxie.cn/ppt/createhttps://www.paperxie.cn/ppt/create 一、开题答辩人破防瞬间:PPT 做得好,答辩分数高一半 “论文写完了,PPT 才是真正的修罗场…...
【仅限JDK 25 Early Access用户】:隐藏API `LinkerOptions` 强制启用向量化调用的2行代码,实测吞吐提升2.8倍
第一章:Java 25 外部函数接口优化案例Java 25 正式将外部函数与内存 API(Foreign Function & Memory API)从预览特性转为正式特性,显著提升了 JVM 与本地代码交互的安全性、性能与开发体验。相比早期 JNI 方案,FFM…...
Leather Dress Collection 企业级参数调优指南:平衡响应速度与生成质量
Leather Dress Collection 企业级参数调优指南:平衡响应速度与生成质量 如果你正在考虑把Leather Dress Collection这类大模型服务搬到公司的生产环境里,那你肯定遇到过这样的纠结:调快了,生成的内容质量好像会打折扣;…...
从零到一:深度解析BertTokenizer.from_pretrained的加载机制与实战技巧
1. 初识BertTokenizer.from_pretrained:你的NLP敲门砖 第一次接触Hugging Face的Transformers库时,我被BertTokenizer.from_pretrained()这个方法深深吸引了。它就像是一把万能钥匙,能快速打开各种预训练语言模型的大门。记得当时我尝试用传统…...
避坑指南:用高德DistrictSearch获取精准行政边界时遇到的5个典型问题(含最新GeoJson处理技巧)
高德DistrictSearch深度避坑:5个实战难题与GeoJson优化方案 当你在深夜调试地图边界数据时,突然发现某个街道的轮廓出现了诡异的锯齿状变形——这不是恐怖片情节,而是使用高德DistrictSearch时可能遇到的真实场景。作为经历过数十个地图项目…...
