当前位置: 首页 > news >正文

Linux多线程【初识线程】

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、什么是线程?
      • 1.1、基本概念
      • 1.2、线程理解
      • 1.3、进程与线程的关系
      • 1.4、简单使用线程
    • 2、重谈地址空间
      • 2.1、页表的大小
      • 2.2、内存与磁盘的交互
      • 2.3、深入页表
      • 2.4、小结
    • 3、线程小结
      • 3.1、再谈线程
      • 3.2、线程的优点
      • 3.3、线程的缺点
      • 3.4、线程的用途
  • 🌆总结


🌇前言

将一份代码成功编译后,可以得到一个可执行程序,程序运行后,相关代码和数据被 load 到内存中,并且操作系统会生成对应数据结构(比如 PCB)对其进行管理及分配资源,准备工作做完之后,我们就可以得到一个运行中的程序,简称为 进程,对于操作系统来说,光有 进程 的概念是无法满足高效运行的需求的,因此需要一种执行粒度更细、调度成本更低的执行流,而这就是 线程

Windows 中的线程

线程


🏙️正文

1、什么是线程?

1.1、基本概念

可能很多人第一次听说 线程 这个词是在 处理器 中,比如今年 英特尔第 13 代酷睿 系列芯片,就在其宣传页中提到了 线程 这个词

图示
硬件上的 线程 概念我们这里不讨论,接下来看看操作系统层面的 线程概念

教材观点

  1. 线程就是一个执行分支、执行粒度比进程更细、调度成本更低
  2. 线程就是进程内部的一个执行流

内核观点

  • 进程是承担系统资源分配的基本实体,而线程是 CPU 运行的基本单位

线程是对以往进程概念的补充完善,正确理解线程概念是一件十分重要的事

1.2、线程理解

注意:以下理解是站在 Linux 系统的角度,不同的系统具体实现方式略有差异

理解 线程 之前需要先简单回顾一下 进程

  • 程序运行后,相关的代码和数据会被 load 到内存中,然后操作系统为其创建对应的 PCB 数据结构、生成虚拟地址空间、分配对应的资源,并通过页表建立映射关系

详见 《Linux进程学习【进程地址】》

图示

进程之间是相互独立

即使是 父子进程,他们也有各自的 虚拟地址空间、映射关系、代码和数据(可能共享部分数据,出现修改行为时引发 写时拷贝机制

如果我们想要创建 其他进程 执行任务,那么 虚拟地址空间、映射关系、代码和数据 这几样东西是必不可少的,想象一下:如果只有进程的概念,并且同时存在几百个进程,那么操作系统调度就会变得十分臃肿

  • 操作系统在调度进程时,需要频繁保存上下文数据、创建的虚拟地址空间及建立映射关系

为了避免这种繁琐的操作,引入了 线程 的概念,所谓 线程 就是:额外创建一个 task_struct 结构,并且该 task_struct 同样指向当前的虚拟地址空间,并且不需要建立映射关系及加载代码和数据,如此一来,操作系统只需要 创建一个 task_struct 结构即可完成调度,成本非常低

为什么切换进程比切换线程开销大得多?
CPU 内部包括:运算器、控制器、寄存器、MMU、硬件级缓存(cache,其中 硬件级缓存 cache 又称为 高速缓存,遵循计算机设计的基本原则:局部性原理,会预先加载 部分用户可能访问的数据 以提高效率,如果切换进程,会导致 高速缓存 中的数据无法使用(进程具有独立性),重新开始 预加载,这是非常浪费时间的(对于 CPU 来说);但切换线程就不一样了,因此线程从属于进程,切换线程时,所需要的数据的不会发生改变,这就意味值 高数缓存 中的数据可以继续使用,并且可以接着 预加载 下一波数据

不同 CPU高速缓存 大小不同,足够大的高速缓存 + 先进的工艺 就可以得到一块性能优越的 CPU
图示

注:高速缓存中预加载的是公共数据,并非线程的私有数据

图示

进程(process)的 task_struct 称为 PCB,线程(thread)的 task_struct 则称为 TCB

从今天开始,无论是 进程 还是 线程,都可以称为 执行流线程 从属于 进程当进程中只有一个线程时,我们可以粗粒度的称当前进程为一个单独的执行流;当进程中有多个线程时,则称当前进程为多执行流,其中每一个执行流都是一个个的线程

执行流的调度由操作系统负责,CPU 只负责根据 task_struct 结构进行运算

  • 若下一个待调度的执行流为一个单独的进程,操作系统仍需创建 PCB 及 虚拟地址空间、建立映射关系、加载代码和数据
  • 但如果下一个待调度的执行流为一个线程,操作系统只需要创建一个 TCB,并将其指向已有的虚拟地址空间即可

现在面临着一个很关键的问题:进程和线程究竟是什么关系?

问号

1.3、进程与线程的关系

进程是承担系统资源分配的实体,比如 程序运行必备的:虚拟地址空间、页表映射关系、相关数据和代码 这些都是存储在 进程 中的,也就是我们历史学习中 进程 的基本概念

线程是 CPU 运行的基本单位,程序运行时,CPU 只认 task_struct 结构,并不关心你是 线程 还是 进程,不过,线程 包含于 进程 中,一个 进程 可以只有一个 线程,也可以有很多 线程,当只有一个 线程 时,通常将其称为 进程,但对于 CPU 来说,这个 进程 本质上仍然是 线程;因为 CPU 只认 task_struct 结构,并且 PCBTCB 都属于 task_strcut,所以才说 线程是 CPU 运行的基本单位

总结:进程是由操作系统将程序运行所需地址空间、映射关系、代码和数据打包后的资源包,而 线程/轻量级线程/执行流 则是利用资源完成任务的基本单位

线程包含于进程中,进程本身也是一个线程

我们之前学习的进程概念是不完整的,引入线程之后,可以对进程有一个更加全面的认识

通常将程序启动,比如 main 函数中的这个线程称为 主线程,其他线程则称为 次线程

图示

实际上 进程 = PCB + TCB + 虚拟地址空间 + 映射关系 + 代码和数据,这才是一个完整的概念

以后谈及进程时,就要想到 一批执行流+可支配的资源

图示

进程与线程的概念并不冲突,而是相互成就

Linux 中,认为 PCBTCB 的共同点太多了,于是直接复用了 PCB 的设计思想和调度策略,在进行 线程管理 时,完全可以复用 进程管理 的解决方案(代码和结构),这可以大大减少系统调度时的开销,做到 小而美,因此 Linux 中实际是没有真正的 线程 概念的,有的只是复用 PCB 设计思想的 TCB

在这种设计思想下,线程 注定不会过于庞大,因此 Linux 中的 线程 又可以称为 轻量级进程(LWP轻量级进程 足够简单,且 易于维护、效率更高、安全性更强,可以使得 Linux 系统不间断的运行程序,不会轻易 崩溃

一切皆文件一样,这种设计思想注定 Linux 会成为一款 卓越 的操作系统

别的系统采用的是其他方案,比如 Windows 使用的是真线程方案,为 TCB 额外设计了一逻辑,这就导致操作系统在同时面临 PCBTCB 时需要进行识别后切换成不同的处理手段,存在不同的逻辑容易增加系统运行不稳定的风险,这就导致 Windows 无法做到长时间运行,需要通过重启来重置风险
此时我的电脑中同时存在几百个进程和几千个真线程,可想而知操作系统的负担有多大

图示

1.4、简单使用线程

如何验证 Linux 中的线程解决方案? 简单使用一下就好了

接下来简单使用一下 pthread 线程原生库中的线程相关函数(只是简单使用,不涉及其他操作)

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadHandler1(void *args)
{while (true){cout << "我是次线程1,我正在运行..." << endl;sleep(1);}
}void *threadHandler2(void *args)
{while (true){cout << "我是次线程2,我正在运行..." << endl;sleep(1);}
}void *threadHandler3(void *args)
{while (true){cout << "我是次线程3,我正在运行..." << endl;sleep(1);}
}int main()
{pthread_t t1, t2, t3; // 创建三个线程pthread_create(&t1, NULL, threadHandler1, NULL);pthread_create(&t2, NULL, threadHandler2, NULL);pthread_create(&t3, NULL, threadHandler3, NULL);// 主线程运行while (true){cout << "我是主线程" << endl;sleep(1);}return 0;
}

编译程序时,需要带上 -lpthread 指明使用 线程原生库

结果:主线程+三个次线程同时在运行

至于为什么打印结果会有点不符合预期,这就涉及到 加锁 相关问题了,后面再解决

图示

使用指令查看当前系统中正在运行的 线程 信息

ps -aL | head -1 && ps -aL | grep myThread | grep -v grep

图示

可以看到此时有 四个线程

  • 细节1:四个线程的 PID 都是 13039
  • 细节2:四个线程的 LWP 各不相同
  • 细节3:第一个线程的 PIDLWP 是一样的

其中,第一个线程就是 主线程,也就是我们之前一直很熟悉的 进程,因为它的 PIDLWP 是一样的,所以只需要关心 PID 也行

操作系统如何判断调度时,是切换为 线程 还是切换为 进程

  • 将待切换的执行流 PID 与当前执行流的 PID 进行比对,如果相同,说明接下来要切换的是 线程,否则切换的就是 进程
  • 操作系统只需要找到 LWPPID 相同的线程,即可轻松锁定 主线程

线程是进程的一部分,给其中任何一个线程发送信号,都会影响到其他线程,进而影响到整个进程


2、重谈地址空间

注:当前部分是拓展,与线程没有很大的关系,但是一个比较重要的知识点

2.1、页表的大小

页表 是用来将 虚拟地址物理地址 之间建立映射关系的,除此之外,页表 中还存在 其他属性 字段

图示

众所周知,在 32 位系统中,存在 2^32 个地址(一个内存单元大小是 1byte),意味着虚拟地址空间 的大小为 4GB

假设极端情况:每个地址都在页表中建立了映射关系,其中页表的每一列大小都是 4 字节,那么页表的大小就是 2^32 * 4 * 3 * 1byte = 48GB,这就意味着悲观情况下页表已经干掉 48GB 的内存了,但现在电脑普遍都只有 16GB 内存,更何况是几十年前的电脑

所以说页表绝对不是采用这种单纯 地址->地址 的映射方案

2.2、内存与磁盘的交互

操作系统从 磁盘 中读取数据时,一次读取大量数据多次读取少量数据 要快的多,因为 磁盘 是外设,每一次读取都必然伴随着寻址等机械运动(机械硬盘),无论是对于 内存 还是 CPU ,这都是非常慢的,为了尽可能提高效率,操作系统选择一次 IO 大量数据的方式读取数据

通常 IO 的数据以 为基本单位,在文件系统中,一个 的大小为 4KB(一个块由8个扇区组成,单个扇区大小为 512Byte),即使我们一次只想获取一个字节,操作系统最低也会 IO 一个 数据块4KB

4KB 这个大小很关键

  • 文件系统/编译器:文件存储时,需要以 4KB 为单位进行存储
  • 操作系统/内存:读取文件或进行内存管理时,也是以 4KB 为单位的

也就是说,内存实际上是被切成大小为 4KB 的小块的,在内存中,单块内存(4KB)被称为 Page,组成单块内存的边界(类似于下标)被称为 页框(页帧)

图示

为了将内存中的 Page 进行管理,需要 先描述,在组织,构建 struct page 结构体,用于描述 Page 的状态,比如是否为脏数据、是否已经被占用了,因为存在很多 Page,所以需要将这些 struct page 结构进行管理,使用的就是 数组(天然有下标) struct page mem[N],其中 N 表示当前内存中的 Page 数量

struct page
{int status; // 基础字段:状态// 注意:这个结构不能设计的太复杂了,因为稍微大一点内存就爆了,所以里面的属性非常少
};struct page mem[N]; // 管理 page 结构体的数组

假设我们的内存为 4GB,那么等分为 4KBPage,可以得到约 100wPage,其中 struct page 结构体不会设计的很大,大小是 字节 级别的,也就是说 struct page mem[100w] 占用的总大小不过 4~5MB,对于偌大的内存来说可以忽略不计

内存管理的本质:

  • 申请:无非就是寻找 mem 数组中一块未被使用的足量空间,将对应的 页 Page 属性设置为已被申请,并返回起始地址(足量空间页框的起始地址)
  • 使用:将磁盘中的指定的 4KB 大小数据块存储至内存中对应的 页 Page
  • 释放:将 页 Page 属性设置为可用状态

关于 mem 数组的查找算法(内存分配算法):LRU、伙伴系统等

重新审视 4KB,为什么内存与磁盘交互的基本单位是 块(4KB

这里就要提一下 局部性原理

图示

局部性原理的特征

  • 现代计算机预加载的理论基础
  • 允许我们提前加载正在访问数据的 相邻或者附加的数据(数据预加载)

局部性原理 的核心在于 预加载,如果没有 局部性原理,那么我们可能今天都用不上电脑,因为如果没有这个原则,那么内存在于磁盘交互时,只能做到用户需要什么,就申请什么,这会直接拉低 CPU 的速度,而速度极快的 磁盘 又非常贵

局部性原理 有效避免了这个问题:用户访问数据时,操作系统不仅会加载用想要访问的数据,同时还会加载当前数据的临近数据,如此一来就可以做到用户访问下一份数据时,不必再次 IO,尽量减少 IO 的次数

  • 合理性:用户访问的数据大多都是具有一定连续性的,比如用户访问 668 号数据,那么他下一次想访问的数据大概是 669 及以后,因此可以提前加载

配合上 4KB 的块大小,可以使得每次 IO 足量的数据,并且有可能会多出,起到 预加载 的效果

所以现在就可以回答为什么是 4KB

  1. IO 的基本单位,内核系统/文件系统 都对其提供了支持
  2. 利于通过 局部性原理 预测数据的命中情况,尽可能提高效率

总结:IO 的基本单位是 4KB,内存实际上被划分成了很多个 4KB 的小块,并存在相应的数据结构对其进行管理

2.3、深入页表

显然,页表 绝对不可能动辄几十个 GB,实际在根据 虚拟地址 进行寻址时,页表 也有自己的设计逻辑

虚拟地址(32 位操作系统) 大小也就是 32 比特位,大概也就是 4Byte,通常将一个 虚拟地址 分割为三份:101012

  • 10虚拟地址中的前 10 个比特位,用于寻址 页表2
  • 10虚拟地址中间的 10 个比特位,用于寻找 页框起始地址
  • 12虚拟地址中的后 12 个比特位,用于定位 具体地址(偏移量)

图示

所以,实际上在通过 页表 进行寻址时,需要用到 两个页表(为了方便演示,仅包含一组 kv 关系):

图示
注:“页表2” 中的 20 表示内存中的下标,即 页框地址

通常将 “页表1” 称为 页目录,“页表2” 称为 页表项

  • 页目录:使用 10 个比特位定位 页表项
  • 页表项:使用 10 个比特位定位 页框地址
  • 偏移量:使用 12 个比特位,在 页 Page 中进行任意地址的寻址

所以即使是每个 物理地址 都被寻址的的极端情况下,页表 总大小不过为:(2^10 + 2^10) * (2^10 + 2^20),大约也就需要 4Mb 大小,即可映射至每一个 物理内存,但实际上 物理内存 并不会被时刻占满,大多数情况下都是使用一部分,因此实际 页表 大小不过 几十字节

像这种 页框起始地址+偏移量 的方式称为 基地址+偏移量,是一种运用十分广泛的思想,比如所谓的 类型(intdoublechar…)都是通过 类型的起始地址+类型的大小 来标识该变量大小的,也就是说我们只需要 获得变量的起始地址,即可自由进行偏移操作(如果偏移过度了,就是越界),这也就解释了为什么取地址只会取到 起始地址

总结:得益于 划分+偏移 的思想,使得页表的大小可以变得很小

扩展:动态内存管理

实际上,我们在进行 动态内存管理(malloc/new 申请堆空间时,操作系统 并没有立即在物理内存中申请空间(因为你申请了可能不会立马使用),而是 先在 虚拟地址 中进行申请(成本很低),当我们实际使用该空间时,操作系统 再去 填充相应的页表信息+申请具体的物理内存

像这种操作系统赌博式的行为我们已经不是第一次见了,比如之前的 写时拷贝,就是在赌你不会修改,这样做的好处就是可以 最大化提高效率,对于内存来说,这种使用时再申请的行为会引发 缺页中断

图示

当用户 动态申请内存 时,操作系统只会在 虚拟地址 中申请,具体表现为 返回一块未被使用的空间起始地址,用户实际使用这块空间时,遵循 查页表、寻址物理内存 的原则,实际进行 查页表 操作时,发现 页表项 没有记录此地址的映射关系,于是就会引发 缺页中断,发出对应的 中断信号,陷入内核态,通过 中断控制器 识别 中断信号 后做出相应的动作,比如这里的动作是:填充页表信息、申请物理内存 ;把 物理内存 准备好后,用户就可以进行正常使用了,整个过程非常快,对于用户来说几乎无感知

图示

同理,在进行 磁盘文件读取 时,也存在 缺页中断 行为,毕竟你打开文件了,并不是立即进行读写操作的

诸如这种 硬件级的中断行为 我们已经在 信号产生 中学过了,即:从键盘按下的那一刻,发出硬件中断信号,中断控制器识别为 键盘 发出的信号后,去 中断向量表 中查找执行方法,也就是 键盘 的读取方法

所以操作系统根本不需要关系 硬件 是什么样子,只需要关心对方是否发出了 信号(请求),并作出相应的 动作(执行方法) 即可,很好的实现了 解耦

对于 内存 的具体情况,诸如:是否命中、是否被占用、对应的 RWX 权限 需要额外的空间对其进行描述,而 页表 中的 其他属性 列就包含了这些信息

图示

内存 进行操作时,势必要进行 虚拟地址到物理地址 之间的转换,而 MMU 机制 + 页表信息 可以判断 当前操作 是否合法,如果不合法会报错

注:UK 权限用于区分当前是用户级页表,还是内核级页表

比如这段代码:

char *ps = "Change World!";
*ps = 'N'; // 此时程序会报错(需要赋值为字符,否则无法编译)

结合 页表、信号 等知识,解释整个报错逻辑:

  • "Change World!" 属于字符常量,存储在字符常量区中,其中的权限为 R
  • char *ps 属于一个指针变量,指向字符常量的起始地址
  • 当我们进行 *ps = "No" 操作时,首先会将字符常量的地址转换为物理地址,在转换过程中,MMU 机制发现该内存权限仅为 R,但 *ps 操作需要 W 权限,于是 MMU 引发异常 -> 操作系统识别到异常,将该异常转换为 信号 -> 并把 信号 发给出现问题的 进程 -> 信号暂时被保存 -> 在 内核态转为用户态 的过程中,进行 信号处理 -> 最终结果是终止进程,也就是报错

程序运行后,就会报错

图示

2.4、小结

所以目前 地址空间 的所有组成部分我们都已经打通了,再次回顾这种设计时,会发现 用户压根不知道、也不需要知道虚拟地址空间之后发生的事,只需要正常使用就好了,当引发异常操作时,操作系统能在 查页表 阶段就进行拦截,而不是等到真正影响到 物理内存 时才报错

图示

所谓的 虚拟地址空间 就是在进行设计时添加的一层 软件层,它解决了 多进程时的物理内存访问问题、也解决了物理内存的保护问题,同时还为用户提供了一个简单的虚拟地址空间,做到了 虚拟与物理 的 完美解耦

图示

这种设计思想就是计算机界著名的 所有问题都可以通过添加一层 软件层 解决,这种思想早在几十年前就已经得到了运用

这种分层结构不仅适用于 操作系统,还适用于 网络,比如大名鼎鼎的 OSI 七层网络模型


3、线程小结

3.1、再谈线程

Linux 中没有 真线程,有的只是复刻 进程 代码和管理逻辑的 轻量级线程(LWP

线程 有以下概念:

  • 在一个程序中的一个执行路线就叫做 线程(Thread),或者说 线程 是一个进程内部的控制程序
  • 每一个进程都至少包含一个 主线程
  • 线程 在进程内部执行,本质上仍然是在进程地址空间内运行
  • Linux 系统中,CPU 看到的 线程 TCB 比传统的 进程 PCB 更加轻量化
  • 透过进程地址空间,可以看到进程的大部分资源,将资源合理分配给每个执行流,就形成了 线程执行流

3.2、线程的优点

线程 最大的优点就是 轻巧、灵活,更容易进行调度

  • 创建一个线程的代价比创建一个进程的代价要小得多
  • 调度线程比调度进程要容易得多
  • 线程占用的系统资源远小于进程
  • 可以充分利用多处理器的并行数量(进程也可以)
  • 在等待慢速 IO 操作时,程序可以执行其他任务(比如看剧软件中的 “边下边看” 功能)
  • 对于计算密集型应用,可以将计算分解到多个线程中实现(比如 压缩/解压 时涉及大量计算)
  • 对于 IO密集型应用,为了提高性能,将 IO操作重叠,线程可以同时等待资源,进行 高效IO(比如 文件/网络 的大量 IO 需要,可以通过 多路转接 技术,提高效率)

线程 的合理使用可以提高效率,但 线程 不是越多越好,而是 合适 最好,让每一个线程都能参与到计算中

3.3、线程的缺点

线程 也是有缺点的:
1、性能损失,当 线程 数量过多时,频繁的 线程 调度所造成的消耗会导致 计算密集型应用 无法专心计算,从而造成性能损失

2、 健壮性降低,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

在下面这个程序中,次线程2 出现异常后,会导致整个进程运行异常,进而终止进程

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;void *threadHandler1(void *args)
{while (true){cout << "我是次线程1,我正在运行..." << endl;sleep(1);}
}void *threadHandler2(void *args)
{while (true){sleep(5); // 等其他线程先跑一会cout << "我是次线程2,我正在运行..." << endl;char *ps = "Change World!";*ps = 'N';}
}int main()
{pthread_t t1, t2; // 创建两个线程pthread_create(&t1, NULL, threadHandler1, NULL);pthread_create(&t2, NULL, threadHandler2, NULL);// 主线程运行while (true){cout << "我是主线程" << endl;sleep(1);}return 0;
}

结果一轮到 次线程2 运行,因为触发异常,从而整个进程就直接终止了

图示

为什么 单个线程 引发的错误需要让 整个进程 来承担?

  • 站在技术角度,完全可以让其自行承担,但这不合理
  • 系统角度:线程是进程的执行分支,线程出问题了,进程也不应该继续运行(比如一颗老鼠屎坏了一锅汤)
  • 信号角度:线程出现异常后,MMU 识别到异常 -> 操作系统将异常转换为信号 -> 发送信号给指定进程,信号的对象是进程,自然无法单发给 线程,进而整个进程也就都终止了

3、缺乏访问控制,进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响

如何证明 轻量级线程 看到的是同一份资源?通过 多进程中,父子进程之间发生写时拷贝的例子验证

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 0;void *threadHandler1(void *args)
{while (true){printf("我是次线程1,我正在运行... &g_val: %p  g_val: %d\n", &g_val, g_val);sleep(1);}
}void *threadHandler2(void *args)
{while (true){printf("我是次线程2,我正在运行... &g_val: %p  g_val: %d\n", &g_val, g_val);g_val++; // 次线程2 每次都需改这个全局变量sleep(1);}
}int main()
{pthread_t t1, t2; // 创建两个线程pthread_create(&t1, NULL, threadHandler1, NULL);pthread_create(&t2, NULL, threadHandler2, NULL);// 主线程运行while (true){printf("我是主线程,我正在运行... &g_val: %p  g_val: %d\n", &g_val, g_val);sleep(1);}return 0;
}

结果:无论是主线程还是次线程,当其中的一个线程出现修改行为时,其他线程也会同步更改

图示

多个线程访问同时访问一个资源,不加以保护的话,势必会造成影响,当然这都是后话了(加锁相关内容)

4、编程难度提高,编写与调试一个多线程程序需要考虑许多问题,诸如 加锁、同步、互斥 的等,面对多个执行流时,调试也是非常困难的

3.4、线程的用途

合理的使用 多线程,可以提高 CPU 计算密集型程序的效率

合理的使用 多线程,可以提高 IO 密集型程序中用户的体验(具体表现为用户可以一边下载,一边做其他事情)

图示


🌆总结

以上就是本次关于 Linux多线程【初识线程】的全部内容了,在本文中,我们主要学习了 线程 的基本概念,深入理解了地址空间,比如 如何页表进行地址的转换,最后复盘了 线程 的基本概念,学习了其优缺点及使用场景,多线程 是一个十分重要的章节,需要用心学习


星辰大海

相关文章推荐

Linux进程信号 ===== :>
【信号产生】、【信号保存】、【信号处理】

Linux进程间通信 ===== :>

【消息队列、信号量】、【共享内存】、【命名管道】、【匿名管道】

Linux基础IO ===== :>

【软硬链接与动静态库】、【深入理解文件系统】、【模拟实现C语言文件流】、【重定向及缓冲区理解】、【文件理解与操作】

Linux进程控制 ===== :>

【简易版bash】、【进程程序替换】、【创建、终止、等待】

Linux进程学习 ===== :>

【进程地址】、【环境变量】、【进程状态】、【基本认知】

Linux基础 ===== :>

【gdb】、【git】、【gcc/g++】、【vim】、Linux 权限理解和学习、听说Linux基础指令很多?这里都帮你总结好了

相关文章:

Linux多线程【初识线程】

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 &#x1f307;前言&#x1f3d9;️正文1、什么是线程&#xff1f;1.1、基本概念1.2、线程理解1.3、进程与线程的关系…...

Python爬虫的应用场景与技术难点:如何提高数据抓取的效率与准确性

作为专业爬虫程序员&#xff0c;我们在数据抓取过程中常常面临效率低下和准确性不高的问题。但不用担心&#xff01;本文将与大家分享Python爬虫的应用场景与技术难点&#xff0c;并提供一些实际操作价值的解决方案。让我们一起来探索如何提高数据抓取的效率与准确性吧&#xf…...

Spring Cloud Gateway系例—参数配置(CORS 配置、SSL、元数据)

一、CORS 配置 你可以配置网关来控制全局或每个路由的 CORS 行为。两者都提供同样的可能性。 1. Global CORS 配置 “global” CORS配置是对 Spring Framework CorsConfiguration 的URL模式的映射。下面的例子配置了 CORS。 Example 77. application.yml spring:cloud:gat…...

QT:UI控件(按设计师界面导航界面排序)

基础部分 创建新项目&#xff1a;QWidget&#xff0c;QMainWindow&#xff0c;QDialog QMainWindow继承自QWidget&#xff0c;多了菜单栏; QDialog继承自QWidget&#xff0c;多了对话框 QMainWindow 菜单栏和工具栏&#xff1a; Bar: 菜单栏&#xff1a;QMenuBar&#xff0…...

AtCoder Beginner Contest 314-A/B/C

A - 3.14 题目要求输出圆周率保留小数几位后的结果 用字符串来存储长串的圆周率&#xff0c;截取字符串就可以了。 #include<iostream> using namespace std; int main() {string s"3.1415926535897932384626433832795028841971693993751058209749445923078164062…...

讯飞星火、文心一言和通义千问同时编“贪吃蛇”游戏,谁会胜出?

同时向讯飞星火、文心一言和通义千问三个国产AI模型提个相同的问题&#xff1a; “python 写一个贪吃蛇的游戏代码” 看哪一家AI写的程序直接能用&#xff0c;谁就胜出&#xff01; 讯飞星火 讯飞星火给出的代码&#xff1a; import pygame import sys import random# 初…...

数学建模之“聚类分析”原理详解

一、聚类分析的概念 1、聚类分析&#xff08;又称群分析&#xff09;是研究样品&#xff08;或指标&#xff09;分类问题的一种多元统计法。 2、主要方法&#xff1a;系统聚类法、有序样品聚类法、动态聚类法、模糊聚类法、图论聚类法、聚类预报法等。这里主要介绍系统聚类法…...

【面试问题】当前系统查询接口需要去另外2个系统库中实时查询返回结果拼接优化思路

文章目录 场景描述优化思路分享资源 场景描述 接口需要从系统1查询数据&#xff0c;查出的每条数据需要从另一个系统2中再去查询某些字段&#xff0c; 比如&#xff1a;从系统1中查出100条数据&#xff0c;每条数据需要去系统2中再去查询出行数据&#xff0c;可能系统1一条数…...

Scada和lloT有什么区别?

人们经常混淆SCADA&#xff08;监督控制和数据采集&#xff09;和IIoT&#xff08;工业物联网&#xff09;。虽然SCADA系统已经存在多年&#xff0c;但IIoT是一种相对较新的技术&#xff0c;由于其能够收集和分析来自各种设备的大量数据而越来越受欢迎。SCADA和IIoT都用于提高工…...

Conda(Python管理工具)

1.简介 Conda是一个开源的包管理器和环境管理器&#xff0c;主要用于管理Python&#xff0c;但也可以用于其他语言。它主要用于安装、管理和更新软件包及其依赖项&#xff0c;以及创建、保存、加载和切换不同的开发环境。Conda可以在Windows、MacOS和Linux系统上使用&#xff…...

(14)嵌套列表,Xpath路径表达式,XML增删查改,Implicit,Operator,Xml序列化,浅拷贝与深拷贝

一、作业问题 1、问&#xff1a;listbox1.items[i]返回的object是指的字符串吗&#xff1f; 答&#xff1a;items是真正的对象集合&#xff0c;在Add时加的是Person对象p&#xff0c;则里面的item就是Person对象p。 但是&#xff0c;在listbox1显…...

软考笔记 信息管理师 高级

文章目录 介绍考试内容与时间教材 预习课程一些例子课本结构考试内容 1 信息与信息化1.1 信息与信息化1.1.1 信息1.1.2 信息系统1.1.3 信息化 1.2 现代化基础设施1.2.1 新型基础建设1.2.2 工业互联网1.2.3 车联网&#xff1a; 1.3 现代化创新发展1.3.1 农业农村现代化1.3.2 两化…...

124、SpringMVC处理一个请求的流程是怎样的?

SpringMVC处理一个请求的流程是怎样的? 一、处理流程二、流程图三、额外扩展(可不看)一、处理流程 Tomcat接收到一个请求后,会交给DispatcherServlet进行处理DispatcherServlet会根据请求的path找到对应的HandlerHandler就是一个加了@RequestMapping的方法,然后就利用反射…...

低成本高收益,五金店小程序的秘密武器

如今&#xff0c;随着移动互联网的快速发展&#xff0c;小程序成为了许多企业进行线上业务拓展的重要方式之一。对于那些不懂代码的人来说&#xff0c;制作一个小程序可能会让人觉得困难重重。但是&#xff0c;现在&#xff0c;借助乔拓云平台&#xff0c;不懂代码的人也能轻松…...

C语言宏定义详解

文章目录 宏定义无参宏定义带参宏定义固定参数宏可变参数宏 多语句宏处理连接符条件判断常见预定义宏 宏在C语言中是一段有名称的代码片段&#xff08;使用#define定义&#xff09;&#xff0c;在预处理阶段会把程序中的宏名替换为对应的代码片段&#xff0c;然后才进入编译阶段…...

SwiftUI 动画进阶:实现行星绕圆周轨道运动

0. 概览 SwiftUI 动画对于优秀 App 可以说是布帛菽粟。利用美妙的动画我们不仅可以活跃界面元素,更可以单独打造出一整套生动有机的世界,激活无限可能。 如上图所示,我们用动画粗略实现了一个小太阳系:8大行星围绕太阳旋转,而卫星们围绕各个行星旋转。 在本篇博文中,您将…...

物理试题-空气净化器

详细解释...

Es、kibana安装教程-ES(二)

上篇文章介绍了ES负责数据存储&#xff0c;计算和搜索&#xff0c;他与传统数据库不同&#xff0c;是基于倒排索引来解决问题的。Kibana是es可视化工具。 分布式搜索ElasticSearch-ES&#xff08;一&#xff09; 一、ElasticSearch安装 官网下载地址&#xff1a;https://www…...

leetcode 917.仅仅反转字母

⭐️ 题目描述 &#x1f31f; leetcode链接&#xff1a;仅仅反转字母 ps&#xff1a; 这道题思路很简单&#xff0c;只需要一个下标在前一个下标在后&#xff0c;分别找是字母的字符&#xff0c;找到之后交换即可。 代码&#xff1a; class Solution { public:bool isAlpha …...

有没有推荐的golang的练手项目?

前言 下面是github上的golang项目&#xff0c;适合练手&#xff0c;可以自己选择一些项目去练习&#xff0c;整理不易&#xff0c;希望能多多点赞收藏一下&#xff01;废话少说&#xff0c;我们直接进入正题>>> 先推荐几个教程性质的项目&#xff08;用于新手学习、巩…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

黑马Mybatis

Mybatis 表现层&#xff1a;页面展示 业务层&#xff1a;逻辑处理 持久层&#xff1a;持久数据化保存 在这里插入图片描述 Mybatis快速入门 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6501c2109c4442118ceb6014725e48e4.png //logback.xml <?xml ver…...

剑指offer20_链表中环的入口节点

链表中环的入口节点 给定一个链表&#xff0c;若其中包含环&#xff0c;则输出环的入口节点。 若其中不包含环&#xff0c;则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)

本期内容并不是很难&#xff0c;相信大家会学的很愉快&#xff0c;当然对于有后端基础的朋友来说&#xff0c;本期内容更加容易了解&#xff0c;当然没有基础的也别担心&#xff0c;本期内容会详细解释有关内容 本期用到的软件&#xff1a;yakit&#xff08;因为经过之前好多期…...

视觉slam十四讲实践部分记录——ch2、ch3

ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...

排序算法总结(C++)

目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指&#xff1a;同样大小的样本 **&#xff08;同样大小的数据&#xff09;**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...