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时间片,在切换之前将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态运行。
相关文章
|
9月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
9月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
381 0
|
9月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
303 26
|
10月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
693 5
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
528 20
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
686 0
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
298 1
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
551 0