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基础指令很多?这里都帮你总结好了

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

双向-->带头-->循环链表

目录 一、双向带头循环链表概述 1.什么是双向带头循环链表 2.双向带头循环链表的优势 3.双向带头循环链表简图 二、双向带头循环链表的增删查改图解及代码实现 1.双向带头循环链表的头插 2.双向带头循环链表的尾插 3.双向带头循环链表的头删 4.双向带头循环链表的尾删…

Linux - 借助 inotifywait,轻松实现 Linux 文件/目录事件监听

文章目录 inotify-tools 依赖包使用示例 inotify-tools 依赖包 [rootVM-24-3-centos ~]# yum install inotify-tools Loaded plugins: fastestmirror, langpacks Repository epel is listed more than once in the configuration Determining fastest mirrors ...... ...... ..…

Python实现透明隧道爬虫ip:不影响现有网络结构

作为一名专业爬虫程序员&#xff0c;我们常常需要使用隧道代理来保护个人隐私和访问互联网资源。本文将分享如何使用Python实现透明隧道代理&#xff0c;以便在保护隐私的同时不影响现有网络结构。通过实际操作示例和专业的解析&#xff0c;我们将带您深入了解透明隧道代理的工…

End-to-End Object Detection with Transformers

DERT 目标检测 基于卷积神经网络的目标检测回顾DETR对比Swin Transformer摘要检测网络流程DERT网络架构编码器概述解码器概述整体结构object queries的初始化Decoder中的Muiti-Head Self-AttentionDecoder中的Muiti-Head Attention 损失函数解决的问题 基于卷积神经网络的目标检…

【数据结构】 ArrayList简介与实战

文章目录 什么是ArrayListArrayList相关说明 ArrayList使用ArrayList的构造无参构造指定顺序表初始容量利用其他 Collection 构建 ArrayListArrayList常见操作获取list有效元素个数获取和设置index位置上的元素在list的index位置插入指定元素删除指定元素删除list中index位置上…

图数据库_Neo4j学习cypher语言_使用CQL命令002_删除节点_删除属性_结果排序Order By---Neo4j图数据库工作笔记0006

然后我们再来看如何删除节点 可以看到首先 我们这里 比如我要删除张三 可以看到 match (n:student) where n.name = "张三" delete n 这样就是删除了student集合中,name是张三的节点 然后我们再来看 如何来删除关系 match (n:student)-[r]->(m:student) where…

MySQL— 基础语法大全及操作演示!!!(下)

MySQL—— 基础语法大全及操作演示&#xff08;下&#xff09;—— 持续更新 三、函数3.1 字符串函数3.2 数值函数3.3 日期函数3.4 流程函数 四、约束4.1 概述4.2 约束演示4.3 外键约束4.3.1 介绍4.3.2 语法4.3.3 删除/更新行为 五、多表查询5.1 多表关系5.1.1 一对多5.1.2 多对…

Doris2.0时代的一些机遇和挑战!

300万字&#xff01;全网最全大数据学习面试社区等你来&#xff01; 上个周五的时候&#xff0c;Doris官宣了2.0版本&#xff0c;除了在性能上的大幅提升&#xff0c;还有一些特性需要大家特别关注。 根据官网的描述&#xff0c;Doris在下面领域都有了长足进步&#xff1a; 日志…

PHP手术麻醉系统源码,自动生成麻醉和护理医疗文书

一套手术麻醉系统源码&#xff0c;可二次开发 手术室麻醉临床信息系统&#xff08;AIMS&#xff09;是应用于医院手术室、麻醉科室的计算机软件系统。该系统针对整个围术期&#xff0c;对病人进行全程跟踪与信息管理&#xff0c;自动集成病人HIS、LIS、RIS、PACS信息&#xff0…

最新ChatGPT网站AI系统源码+详细图文搭建教程/支持GPT4.0/AI绘画/H5端/Prompt知识库/

一、前言 SparkAi系统是基于国外很火的ChatGPT进行开发的Ai智能问答系统。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。 那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧&#xff01…

图解二叉树,拿下拿下!

图文详解二叉树 一、树形结构概念特性二、树形结构基本概念术语三、树的存储结构四、二叉树 概念与特性五、特殊的二叉树六、二叉树的性质七、二叉树的存储结构八、二叉树的基本操作1、二叉树的遍历&#xff08;1&#xff09;前中后序遍历&#xff08;2&#xff09;经典找序列&…

性能优化的重要性

性能优化的重要性 性能优化的重要性摘要引言注意事项代码示例及注释性能优化的重要性 性能优化的重要性在 Java 中的体现响应速度资源利用效率扩展性与可维护性并发性能合理的锁策略线程安全的数据结构并发工具类的应用避免竞态条件和死锁 总结代码示例 博主 默语带您 Go to Ne…

pytest框架快速进阶篇-pytest前置和pytest后置,skipif跳过用例

一、Pytest的前置和后置方法 1.Pytest可以集成unittest实现前置和后置 importunittestimportpytestclassTestCase(unittest.TestCase):defsetUp(self)->None:print(unittest每个用例前置)deftearDown(self)->None:print(unittest每个用例后置)classmethoddefsetUpClass…

01- 中断

中断 中断1.1 NVIC中断优先级分组1.2 外部中断<1> 映射中断线<2> 设置中断触发方式<3> 编写中断服务函数外部中断常用的库函数&#xff1a;中断函数初始化外部中断的一般配置步骤&#xff1a;部分示例&#xff1a; 中断 1.1 NVIC中断优先级分组 《stm32中文…

netty学习分享 二

操作系统IO模型与实现原理 阻塞IO 模型 应用程序调用一个IO函数&#xff0c;导致应用程序阻塞&#xff0c;等待数据准备好。如果数据没有准备好&#xff0c;一直等待….数据准备好了&#xff0c;从内核拷贝到用户空间,IO函数返回成功指示。 当调用recv()函数时&#xff0c;系…

干翻Dubbo系列第十一篇:Dubbo常见协议与通信效率对比

文章目录 文章说明 一&#xff1a;协议 1&#xff1a;什么是协议 2&#xff1a;协议和序列化关系 3&#xff1a;协议组成 &#xff08;一&#xff09;&#xff1a;头信息 &#xff08;二&#xff09;&#xff1a;体信息 4&#xff1a;Dubbo3中常见的协议 5&#xff1a;…

【数据结构与算法】十大经典排序算法-希尔排序

&#x1f31f;个人博客&#xff1a;www.hellocode.top &#x1f3f0;Java知识导航&#xff1a;Java-Navigate &#x1f525;CSDN&#xff1a;HelloCode. &#x1f31e;知乎&#xff1a;HelloCode &#x1f334;掘金&#xff1a;HelloCode ⚡如有问题&#xff0c;欢迎指正&#…

python爬虫5:requests库-案例3

python爬虫5&#xff1a;requests库-案例3 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 申明 ​ 本系列所涉及的代码仅用于个人研究与讨论&#xff0c;并不会对网…

3.微服务概述

1.大型网络架构变迁 SOA与微服务最大的差别就是服务拆分的细度&#xff0c;目前大多数微服务实际上是SOA架构&#xff0c;真正的微服务应该是一个接口对应一个服务器&#xff0c;开发速度快、成本高&#xff1b; 微服务SOA能拆分的就拆分是整体的&#xff0c;服务能放一起的都…

生活随笔,记录我的日常点点滴滴.

前言 &#x1f618;个人主页&#xff1a;曲终酣兴晚^R的小书屋&#x1f971; &#x1f615;作者介绍&#xff1a;一个莽莽撞撞的&#x1f43b; &#x1f496;专栏介绍&#xff1a;日常生活&往事回忆 &#x1f636;‍&#x1f32b;️每日金句&#xff1a;被人暖一下就高热&…