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时间片,在切换之前将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态运行。
相关文章
|
3月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
145 0
|
8月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
3月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
4月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
251 5
|
8月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
264 20
|
8月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
10月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
162 1
|
11月前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
680 7
|
11月前
|
消息中间件 存储 安全
|
12月前
|
存储 并行计算 安全
C++多线程应用
【10月更文挑战第29天】C++ 中的多线程应用广泛,常见场景包括并行计算、网络编程中的并发服务器和图形用户界面(GUI)应用。通过多线程可以显著提升计算速度和响应能力。示例代码展示了如何使用 `pthread` 库创建和管理线程。注意事项包括数据同步与互斥、线程间通信和线程安全的类设计,以确保程序的正确性和稳定性。
230 5