从线程原理的角度来看C++内存的使用

news2024/11/28 10:41:08

文章目录

  • 线程的内存结构
    • 栈帧
    • 线程/进程调度
  • 线程的进一步使用
    • 线程安全和可重入
    • 一般的内存使用
      • static变量
      • 使用new关键字,访问堆上的内存
    • 类中的内存使用

从上一篇文章来看,线程的使用是比较简单的。但是在c++环境下使用线程,最难也是最麻烦的点在于对内存的管理。因为如果多线程独立运行的话(不是一个一个的join,串行执行),那么对于一些公共的指针,也就是说在不同线程中指向相同位置的内存区域的开辟与释放,就是非常麻烦的事情。因为你不知道哪个线程会先执行到这个位置,把内存开辟了(顺序乱了的话就容易造成野指针,造成内存泄漏)。如果是释放顺序错了的话,就会导致非法访问已经释放的内存区域,造成内存越界,程序直接core dump。
所以这一篇打算说一说线程和内存的关系。

线程的内存结构

首先,线程本身也是一种数据结构,这个数据结构中保持的数据,很多都是用于操作系统用于做线程调度使用的,当然也还有一些是其他的作用,这一小节我们就先来看一下线程这个实体(entity)在程序运行时,在内存中的结构。

std C++11标准库中的thread也需要基于具体的平台的实现,不同平台的实现都会不一样。这里只提一些通用的部分。因为大部分程序还是在linux下开发,所以更多的源代码部分都是基于linux操作系统的。基本上都是基于POSIX标准。

栈帧

我在之前的一篇关于内存的文章中提到过C++程序运行时在内存中的结构:
https://blog.csdn.net/pcgamer/article/details/128148962?spm=1001.2014.3001.5501
其中有一个区域叫做“栈区”,线程的数据就保存在这个区域。

首先需要理解一下,所有的线程运行都是执行一行一行的代码,或者说可以执行一个一个的函数。这些函数和代码都是保存在内存的代码区域的。
或者说函数是一个静态的概念,而线程是一个动态的概念。函数代码通过线程这个动态实体,被操作系统调度到cpu上运行。

所以,首先看一下一个函数被调用时在内存中的结构(不管是单线程还是多线程,都会调用到函数)
在这里插入图片描述

具体的结构,有兴趣的朋友可以去翻翻源码。

函数调用的过程可以概括为:

  • 操作系统创建一个栈帧结构
  • 将返回地址赋值到栈帧结构中(当前函数的下一条代码地址,代码区域中的地址,用于函数返回用)
  • 将行参从右往左的顺序入栈(C代码的标准,其他的不一定,有些地方使用__stdcall这个宏定义就是干的这个)
  • 为其他变量什么的赋值
  • 将栈帧入栈

那么函数结束返回的过程可以概括为:

  • 将函数的返回值保存在eax寄存器中
  • 将当前栈帧出栈,从栈中取到返回地址,并跳转到该位置
  • 将当前栈顶的地址给esp寄存器中,就是下一个栈帧。

从上面的描述来看,我们在一个函数中定义的局部变量(指针除外)是跟随着该函数的栈帧创建而创建;函数执行完毕后,栈帧出栈被清除也就被清除了。
但是如果是函数本地变量的指针,如果使用了new等操作符分配了堆上的内存的话,该指针随着函数结束被清除,但是指针指向的地址是不会被释放的,就会造成内存泄漏。

从多线程的角度来看,可以知道:

  • 函数中的本地变量(非指针)是安全的,不会因为多线程的调用而冲突,所以上一篇中讲到的创建线程的传参方式是值传递,就是保证了这个。
  • 如果在函数中使用了指针,那么就要悠着点,因为你不知道你哪个线程先被调用,如果提前释放了,就会导致错误。
  • 如果是静态变量,那么会发生不一致的情况发生,同样是因为线程的调度问题。

所以,确定线程的调度逻辑,是用好多线程的一个基础。

线程/进程调度

前面说到,线程实际上就是把一个一个的函数代码放到cpu上去运行,那么可以理解为线程就是函数被操作系统调度的一个实体。

大概是下图这个关系:
在这里插入图片描述

  • 操作系统中有一个任务调度的实体,在linux上的结构为task_struct,这个结构维护了一个调度线程的队列及其相关信息,比如线程的状态,内存地址等信息。
  • 当一个函数被调用时(其实就可以是一个线程,因为函数被调用,肯定是一个线程这样一个动态的概念才能被调用),操作系统创建一个栈帧,把函数的地址,行参等信息入栈,创建局部变量入栈等操作。
  • ESP寄存器,这个寄存器一般都是用于记录上一次栈帧的地址,也就是上一次函数的返回位置。当调用一个新函数时,把这个ESP中的地址记录到task结构中,然后把最新的栈帧地址填入到ESP中。
  • 当函数执行完成后,ESP将上一步记录到task结构中的地址拿出来填上,再把当前栈区空间中的栈顶的栈帧退栈,即可完成函数的调用和返回(当然还有一些其他的清理工作)

线程的进一步使用

线程安全和可重入

线程的使用会涉及到两个概念:线程安全和可重入。
这两个概念有很多中解释法,可重入(reentrancy)在wiki上还能找到一个词条: In computing, a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution and then safely called again (“re-entered”) before its previous invocations
complete execution. The interruption could be caused by an internal action such as a jump or call, or by an external action such as a hardware interrupt or signal. Once the reentered invocation completes, the previous invocations will resume correct execution.
简单来说,就是一个线程执行这个方法时,被中断程序打断后,重新恢复运行时不出错。

而线程安全的含义一般指的是,多个线程在调用同一段代码的时候,所有的线程都可以得到正确的结果。

从上面两个描述上来看,个人觉得两个概念基本上差不多,本质上都是对内存中的变量进行访问时不出问题,能得到预期的结果。

所以这里重点说说多线程中内存的使用。

一般的内存使用

从上面栈帧和调度的分析来看,如果一个函数使用的都是局部变量,那么在多线程中肯定是安全的,因为这些变量的内存都是随着线程的调度,而在栈帧中统一被调入和调出。调出后其他的线程也是无法访问和修改的,所以肯定是安全的,也是能得到预期的结果的。
但是,绝大部分场景下,多个线程的合作肯定是会要访问同一个变量或者结构,也就是访问同一块内存的,不考虑类的情况下,有如下的几种情况:

static变量

static变量为在全局数据区分配内存,所有的线程都可以访问到,所以如果要保证得到预期结果,必须根据业务需求在变量的修改上增加锁控制。

使用new关键字,访问堆上的内存

  • 如果在多线程函数内部使用new函数。用于保存内存地址的指针需要时局部变量:

    void pFunc()
    {
      char * cPtr = new char[1024];
      
      // do something
    
      delete cPtr; 
    }
    

    这样的话,虽然内存是在堆上,但是这个指针是跟随着栈帧走的,不会存在被其他线程修改的情况。
    有一个风险就在于在执行到一半的时候被父线程或者其他原因kill掉,导致内存泄漏。

    如果cPtr是一个static的话,那么就很有可能在一个的线程中被delete后,再由某一个线程进行访问,在多线程下就是不安全的。

  • 如果是通过函数的行参传入到函数内,这种情况多见于回调函数的使用中。
    因为回调函数很多情况下是由其他函数调用回来的,而且一般来说都是由事件驱动,基于多线程的。
    在这种情况下,一般来说遵守一个原则:指针参数变量最好是设置为const变量

    void pFunc(const char * cPtr)
    {
      
      // do something
    
    }
    

    因为在这种情况下,这个指针肯定是由外部调用来创建和初始化的,根据谁创建,谁修改,谁销毁的原则。在多线程函数内部,最好不要对这个指针指向的内存做修改和销毁。
    另外,如果要使用的话,尽量在函数的的最开始就单独创建一个属于线程函数自身的内存,并将数据拷贝过来进行使用。

    void pFunc(const char * cPtr, int size)
    {
      char * _cPtr = new char[size];
    
      memset(_cPtr, 0, size);
      memcpy(_cPtr, cPtr, size);
    
      // do something
    
      delete [] _cPtr;
    
      ...
    }
    

类中的内存使用

如果使用到类,和纯面向过程的代码有一点区别:

  • 首先类中的方法来说,在前面的一篇文章提到过,类的方法实际上在符号表中也是会形成一个和普通方法相同的符号,只不过是根据类名,行参等名字做了一些前缀和后缀,所以线程调用起来和普通的方法是一样的,也是通过栈帧的方式来传递。
    至于方法中的局部变量什么的就和上面提到的是一样的了。
  • 类中的成员静态成员变量:所有类对象共有,位于内存的静态区,和上面的静态变量使用方式类似。
  • 类普通成员变量(非指针,指针的要参考具体是怎么创建的,如果是new的话,就都是在堆上)。成员变量的话是在类对象创建和初始化的时候来确定位置的。那么:
    • 如果类对象是通过new的方式来生成的,那么所有的类成员都一起在堆上生成。
    • 如果类对象是作为函数的局部变量来声明的,那就是在栈区,也就是跟随着这个函数的栈帧一起被调度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/331486.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云原生安全2.X 进化论系列|揭秘云原生安全2.X的五大特征

随着云计算技术的蓬勃发展,传统上云实践中的应用升级缓慢、架构臃肿、无法快速迭代等“痛点”日益明显。能够有效解决这些“痛点”的云原生技术正蓬勃发展,成为赋能业务创新的重要推动力,并已经应用到企业核心业务。然而,云原生技…

Mysql 数据类型

1、数值数据类型 1.1 整数类型(精确值) INTEGER, INT, SMALLINT, TINYINT, MEDIUMINT, BIGINT MySQL支持SQL标准的整数类型INTEGER (或INT)和SMALLINT。作为标准的扩展,MySQL还支持整数类型TINYINT、MEDIUMINT和BIGINT。下表显示了每种整数类型所需的存储和范围。…

13.计算机视觉

13.计算机视觉 目录 图像增广 常用的图像增广方法 翻转和裁剪改变颜色结合多种图像增广方法 使用图像增广进行训练 多GPU训练 总结 微调 步骤 热狗识别获取数据集定义和初始化模型微调模型 总结 目标检测和边界框 边界框总结 目标检测数据集 下载数据集读取数据集演示总结 锚…

【3】深度学习之Pytorch——如何使用张量处理表格数据集(葡萄酒数据集)

张量是PyTorch中数据的基础。神经网络将张量输入并产生张量作为输出,实际上,神经网络内部和优化期间的所有操作都是张量之间的操作,而神经网络中的所有参数(例如权重和偏差)也都是张量。 怎样获取一条数据、一段视频或…

Java面试知识点

工作也有好些年了,从刚毕业到前几年看过无数的面试题,总想着自己写一个面试总结,随着自我认识的变化,一些知识点的理解也越来越不一样了。写下来温故而知新。很多问题可能别人也总结过,但是答案不尽相同,如…

纯css实现loading加载中(多种展现形式)

前言 现如今网页越来越趋近于动画,相信大家平时浏览网页或多或少都能看到一些动画效果,今天我们来做一个有意思的动画效果,纯 css 实现 loading 加载中(多种展现形式),下面一起看看吧。 1. 常规 loading 实…

Linux系统之cuda 11情况下如何配置pytorch 10.2

由于目前pytorch1.8.2只能支持到10.2的版本,但ubuntu最新的系统驱动直接支持了cuda 11.4, 并且cuda tooklit支持的默认下载也是11.0。1、确认NVIDIA驱动安装lspci|grep NVIDIA1. 需要先降低cuda tooklit的版本(卸载新版本)cuda-uninstaller in /usr/loca…

统一附件存储MINIO部署使用

一、基于docker环境部署 1、创建docker-compose配置文件 1)创建 docker-compose-minio.yml文件,内容如下: version: 3.7# Settings and configurations that are common for all containers x-minio-common: &minio-commonimage: quay…

结构体+枚举+联合体

目录 一、结构体的声明 (一)结构的基础知识 (二)结构的声明 (三)特殊的声明 (四)结构的自引用 1. 一个结构体内部包含一个类型为该结构本身的成员(不合法&…

Day19 C++STL入门基础知识十一——map、multimap容器 构造赋值、大小交换、插入删除、查找统计、排序【全面深度剖析+例题代码展示】

💃🏼 本人简介:男 👶🏼 年龄:18 ✍每日一句:【道固远,笃行可至;事虽巨,坚为必成】 文章目录1. 基本概念2. 构造赋值① 函数原型② 代码展示③ 测试结果3. 大小…

基于tensorflow的垃圾分类系统

项目描述 该项目基于PySide2和PyQt5设计界面UI,搭配QT Designer进行界面设计。 基于TensorFlow中的Keras模型,进行垃圾分类模型的训练。 项目包含功能有:使用者注册登录功能、管理员训练模型、用户使用模型进行分类。 功能介绍 一、注册登…

JVM调优

JVM调优-VisualVmVisualVm/ Jconsule远程连接第一种方式第二种方式:java 11开启远程GC连接如果还连不上考虑防火墙拦截了端口firewall-cmd --list-all,查看一下并暴露对应端口连接配置VisualVm界面简介采集GC信息的一些命令垃圾回收器切换VisualVm/ Jconsule远程连接…

unity 框选目标

先制作选框: 创建一个Image,给Sourece Image随便添加一张方形图片,如果添加圆的出来就是圆,这个看情况而定,然后勾掉Fill Center这样就镂空了 这种框选一般都是作为组件存在所以代码要做成单例类,默认情况…

【Mysql第十期 数据类型】

文章目录1. MySQL中的数据类型2.类型介绍2.2 可选属性2.2.2 UNSIGNED2.2.3 ZEROFILL2.3 适用场景2.4 如何选择?3. 浮点类型3.2 数据精度说明3.3 精度误差说明4. 定点数类型4.1 类型介绍4.2 开发中经验5. 位类型:BIT6. 日期与时间类型6.1 YEAR类型6.2 DAT…

程序的编译与链接(C语言为例) #代码写好后到运行期间要经过怎样的过程呢?# 粗略版 #

编译与链接前言程序的环境程序的编译与链接写在最后前言 每当我们运行一段代码时,编译器都会自动的帮我们编译代码并将代码转换为一个二进制可执行文件(.exe), 有了这个可执行文件,便可以执行我们写的程序了。那么编译…

Linux-Ubuntu18.04安装anaconda及python解释器环境的配置

1.anaconda的下载anaconda官网搜索链接,点击下载注意:anaconda的下载位置2.anaconda的安装利用如下命令进行安装:$ bash /home/xiaowang/下载/Anaconda3-2022.10-Linux-x86_64.sh一直点击回车enter,阅读文件内容文件阅读完毕&…

canal五部曲-如何保证消息的顺序

分析CanalRocketMQProducer.send canal发送消息到RocketMQ使用到了partitionNum、partitionHash 通过partitionHash可以把消息发送到RocketMQ的不同分区上,因为同一个分区在消费时有序的 public void send(final MQDestination destination, String topicName, com.…

2020年因果推断综述《A Survey on Causal Inference》

最近阅读了TKDD2020年的《A Survey on Causal Inference》,传送门,自己对文章按照顺序做了整理,同时对优秀的内容进行融合,如有不当之处,请多多指教。 文章对因果推理方法进行了全面的回顾,根据传统因果框…

威胁情报是什么

文章目录前言一、威胁情报是什么?数据与情报IOC二、威胁情报的分类1.战略情报2.技术情报3.战术情报4.运营情报三、总结四、参考前言 只要有斗争冲突,就有那些研究、分析和努力去了解对手的人。一场战争的输赢,取决于你对对手的了解&#xff0…

springboot启动过程源码

概述版本<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.3.RELEASE</version><relativePath/></parent>启动入口代码package com.ybjdw.tool;i…