c++理论篇——初窥多线程(一) 计算机内存视角下的多线程编程

简介: c++理论篇——初窥多线程(一) 计算机内存视角下的多线程编程

虚拟地址空间

前言

如果我们比较了解计算机操作系统的话,不难知道操作系统主要有以下四种特征:

  • 并发
  • 共享
  • 虚拟
  • 异步

什么是虚拟呢?指的是在计算机操作系统设计时,为了提高对有限空间时间片的利用,我们一般会选择尝试将一个物理上的实体转换为逻辑上的对应物,我们将这种技术称为虚拟技术,而根据使用目的的不同,我们又将虚拟技术分为两种:时分复用技术空分复用技术

而什么是空分复用技术呢?空分复用技术又叫虚拟处理器技术,它指的是我们将物理存储器转换为虚拟存储器,在逻辑上扩充存储器的容量。而提到空分复用技术就不能不提到我们今天的要说的虚拟内存地址了。

什么是虚拟内存

虚拟内存的概念比较晦涩难懂,从字面意思来解释的话主要是以下几点:

  • 虚拟内存可以用来加载数据,一般是物理内存不够存放的话会放到虚拟内存中
  • 虚拟内存所对应的是一段连续的内存地址,起始位置为0(注意:之所以说虚拟,是因为这个起始位置是被虚拟出来的,并不是物理内存的0地址)
    虚拟内存的大小一般也是由操作系统所决定的,比如32位操作系统的虚拟地址空间大小为2^32位,64位操作系统的大小则是2^64位,每当我们在电脑上运行一个可执行程序的时候,就会得到一个进程,内核会给每一个运行的进程创建一块独属于它们的虚拟内存地址空间,并且将应用程序的数据装载到虚拟地址空间对应的地址上。

我们知道进程在运行的时候指令都是由cpu处理完成的,但是我们知道CPU本身是不具有数据存储功能的,数据的取出与存入都是通过物理内存来实现的 ,而这的实现主要就是依托于CPU中的内存管理单元MMU实现物理内存与虚拟内存地址之间的映射。

虚拟内存的意义

那么问题来了,为什么我们不直接使用物理内存而是选择使用虚拟内存地址呢?我们先来看如果将数据直接加载到物理内存中会发生什么:

假设计算机的物理内存大小为1G, 进程A需要100M内存因此直接在物理内存上从0地址开始分配100M, 进程B启动需要250M内存, 因此继续在物理内存上为其分配250M内存, 并且进程A和进程B占用的内存是连续的。之后再启动其他进程继续按照这种方法进行物理内存的分配……

这样做可能会出现以下的问题:

  • 应用直接访问物理内存,可能会存在恶意软件通过内存寻址来修改进程的内存数据,哪怕没有恶意程序,可能程序出现了一个Bug就会导致进程的内存数据被修改,不利于数据安全。
  • 直接使用内存的话,一个进程所对应的内存是一整块的,如果物理内存不够的话,一般我们会将不常用的进程拷贝到虚拟内存的交换分区,现在就需要直接移动到硬盘了,一方面我们需要将进程一整个移动走,另一方面内存和磁盘之间拷贝时间就会很长,效率低下。
  • 物理内存的使用情况一直在动态的变化,我们无法确定内存现在使用到哪里了,如果直接将程序数据加载到物理内存,内存中每次存储数据的起始地址都是不一样的,这样数据的加载都需要使用相对地址,加载效率低。

而我们使用虚拟内存就可以避免上面的问题了,虚拟地址空间就是一个中间层,相当于在程序和物理内存之间设置了一个屏障,将二者隔离开来。程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。

虚拟内存的分区

从操作系统层面上来说,我们一般会将虚拟内存分为两部分:

  • 内核区
  • 用户区
    这里我们主要介绍一下用户区的组成,用户区的组成主要有九个部分:
  1. env:环境变量,主要是存储与进程相关的环境变量,比如有时候我们导入动态库的时候就需要配置环境变量
  2. 命令行参数:主要指的是我们代码中main函数的部分参数,也就是argc,argv
  3. stack(栈): 存储函数内部声明的非静态局部变量函数参数函数返回地址等信息,栈内存由编译器自动分配释放。栈和堆相反地址“向下生长”,分配的内存是连续的。
  4. 堆(heap):用来存放进程运行时动态分配的内存
  • 堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
  • 堆向高地址扩展(即“向上生长”),是不连续的内存区域。这是由于系统用链表来 存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
  1. .bss段:存储未被初始化的全局变量与静态变量,系统会自动将其初始化为0
  2. .data段:存储已经被初始化的全局变量与静态变量,属于静态存储区,可读可写。
  3. .text段: 代码段也称正文段或文本段,通常用于存放程序的执行代码(即CPU执行的机器指令),代码段一般情况下是只读的,这是对执行代码的一种保护机制。
  4. 保留区: 位于虚拟地址空间的最底部,未赋予物理地址。任何对它的引用都是非法的,程序中的空指针(NULL)指向的就是这块内存地址。

线程的概念

线程是一种轻量级的进程,在Linux系统下其实线程的本质还是进程,我们在计算机上运行的程序是一组指令以及指令参数的组合,指令会按照我们所设计的逻辑区执行,操作系统会根据进程为单位来分配系统资源,我们可以这样去理解:线程是操作系统调度执行的最小单位,进程是系统进行资源分配的最小单位

拓展:

其实除了进程与线程之外还有协程,但是协程并不是相对于操作系统而言的,它由程序员控制调度,本身在运行与使用的过程中是不涉及操作系统内核状态的变化的。

文章的最后我们在来看一下线程与进程之间的区别:

  • 进程有自己独立的地址空间,多个线程共用一个地址空间
  • 在这个地址空间中每个线程都有自己的栈区与寄存器
  • 地址空间中多个线程共享: 代码段, 堆区, 全局数据区, 打开的文件(文件描述符表)
  • 线程是程序执行的最小单位,一个地址空间可以划分出多个线程,在充足的资源基础上我们可以抢占更多的CPU时间片,同时相对于进程,线程上下文切换要更快。
    注意:
    上下文:进/线程复用CPU时间片,在切换之前将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态运行。
相关文章
|
7天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
25 4
|
8天前
|
存储 监控 Java
深入理解计算机内存管理:优化策略与实践
深入理解计算机内存管理:优化策略与实践
|
22天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
16 3
|
22天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
15 2
|
22天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
22天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1
|
27天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
30天前
|
监控 安全 算法
线程死循环确实是多线程编程中的一个常见问题,在编码阶段规避潜在风险
【10月更文挑战第12天】线程死循环确实是多线程编程中的一个常见问题,在编码阶段规避潜在风险
45 2
|
1月前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
36 2
|
30天前
|
存储 C语言 C++
【C++打怪之路Lv6】-- 内存管理
【C++打怪之路Lv6】-- 内存管理
37 0
【C++打怪之路Lv6】-- 内存管理