Linux多线程【初识线程】

news2024/9/23 15:27:40

✨个人主页: 北 海
🎉所属专栏: 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基础指令很多?这里都帮你总结好了

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[oneAPI] Neural Style Transfer

[oneAPI] Neural Style Transfer oneAPINeural Style Transfer特殊环境定义使用包加载数据Neural Style Transfer模型与介绍训练过程结果 比赛&#xff1a;https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0517 Intel DevCloud for oneAPI&#xff1a;https://devcl…

1609.奇偶数

目录 一、题目 二、代码 三、完整测试代码 一、题目 1609. 奇偶树 - 力扣&#xff08;LeetCode&#xff09; 二、代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0),…

【大数据Hive】hive 事务表使用详解

目录 一、前言 二、Hive事务背景知识 hive事务实现原理 hive事务原理之 —— delta文件夹命名格式 _orc_acid_version 说明 bucket_00000 合并器(Compactor) 二、Hive事务使用限制 参数设置 客户端参数设置 客户端参数设置 三、Hive事务使用操作演示 操作步骤 客…

电脑msvcr120.dll丢失怎么修复,msvcr120.dll怎么安装?

msvcr120.dll是Microsoft Visual C Redistributable的一部分&#xff0c;它是Windows操作系统中的一个动态链接库文件。这个文件包含了一些用于C编程的函数和资源&#xff0c;它们被许多应用程序用于提供特定的功能和服务。如果你在运行某个程序时遇到了缺少msvcr120.dll的错误…

AlexNet中文翻译

ImageNet classification with deep convolutional neural networks 原文链接&#xff1a;https://dl.acm.org/doi/abs/10.1145/3065386 目录 使用深度卷积神经网络进行 ImageNet 分类 摘要 1 简介 2 数据集 3 架构 3.1 ReLU非线性 3.2 多GPU上的训练 3.3 局部响应标准化 3.4 重…

centos安装elasticsearch7.9

安装es 下载elasticsearch安装包解压安装包,并修改配置文件解压进入目录修改配置文件 添加用户&#xff0c;并修改所有者切换用户&#xff0c;运行es如何迁移旧版本的数据 下载elasticsearch安装包 下载地址如下&#xff0c;版本号可以替换成自己想要的。 这里需要注意一点&am…

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

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

上海亚商投顾盘:沪指震荡反弹 机器人概念股掀涨停潮

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 三大指数今日震荡反弹&#xff0c;科创50盘中涨超1%。机器人概念股掀涨停潮&#xff0c;通力科技、昊志机电、哈焊…

java接口导出csv

1、背景介绍 项目中需要导出数据质检结果&#xff0c;本来使用Excel&#xff0c;但是质检结果数据行数过多&#xff0c;导致用hutool报错&#xff0c;因此转为导出csv格式数据。 2、参考文档 https://blog.csdn.net/ityqing/article/details/127879556 工程环境&#xff1a;…

Spring Clould 网关 - Gateway

视频地址&#xff1a;微服务&#xff08;SpringCloudRabbitMQDockerRedis搜索分布式&#xff09; Gateway网关-网关作用介绍&#xff08;P35&#xff09; Spring Cloud Gateway 是 Spring Cloud 的一个全新项目&#xff0c;该项目是基于 Spring 5.0&#xff0c;Spring Boot 2…

The coming up production issues

Introduction Ladies and gentlemen, give it up for the wonderful world of software production ! Now, I know that what youre thinking. "Software production!?" That sounds exciting, well, let me tell you, its a rollercoaster(过山车、剧烈起伏的事物…

【第六讲---非线性优化】

优化与优化库 优化问题 &#x1f449;优化问题组成 优化对象目标函数/损失函数/评价函数约束条件 &#x1f449;分类 可以分为凸优化和非凸优化 什么是凸优化呢&#xff1f; 目标函数是凸的&#xff08;有单一极值点称为是凸的&#xff09;不等式约束是凸的所在的空间是凸…

容器和云原生(二):Docker容器化技术

目录 Docker容器的使用 Docker容器关键技术 Namespace Cgroups UnionFS Docker容器的使用 首先直观地了解docker如何安装使用&#xff0c;并快速启动mysql服务的&#xff0c;启动时候绑定主机上的3306端口&#xff0c;查找mysql容器的ip&#xff0c;使用mysql -h contain…

【脚踢数据结构】图(纯享版)

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;软件配置等领域博主&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff01;送给自己和读者的…

Pytest和Unittest测试框架的区别?

如何区分这两者&#xff0c;很简单unittest作为官方的测试框架&#xff0c;在测试方面更加基础&#xff0c;并且可以再次基础上进行二次开发&#xff0c;同时在用法上格式会更加复杂&#xff1b;而pytest框架作为第三方框架&#xff0c;方便的地方就在于使用更加灵活&#xff0…

Python Django 模型概述与应用

今天来为大家介绍 Django 框架的模型部分&#xff0c;模型是真实数据的简单明确的描述&#xff0c;它包含了储存的数据所必要的字段和行为&#xff0c;Django 遵循 DRY Principle 。它的目标是你只需要定义数据模型&#xff0c;然后其它的杂七杂八代码你都不用关心&#xff0c;…

龙迅LT9711 2PORT MIPI或者LVDS转TYPE-C

LT9711 1.描述&#xff1a; Lontium LT9711是双端口MIPI/LVDS到DP1.2转换器&#xff0c;内部有c型替代模式开关和PD控制器。MIPI DSI/CSI输入具有可配置的单端口或双端口&#xff0c;具有1个时钟通道&#xff0c;1个~4个数据通道&#xff0c;最大运行2Gbps/通道&#xff0c;可…

2023 年值得关注的 8 个最佳免费开发者工具

开发者工具对开发人员的重要性不言而喻&#xff0c;保持最新工具的更新可以显著提高你的工作效率并简化您的工作流程。随着技术的快速发展&#xff0c;新的开发工具不断被引入市场。今天&#xff0c;我们将分享 2023 年你值得关注的最新开发者工具。 1.Plaky Plaky 是一种基于…

JVM——JDK 监控和故障处理工具总结

文章目录 JDK 命令行工具jps:查看所有 Java 进程jstat: 监视虚拟机各种运行状态信息 jinfo: 实时地查看和调整虚拟机各项参数jmap:生成堆转储快照**jhat**: 分析 heapdump 文件**jstack** :生成虚拟机当前时刻的线程快照 JDK 可视化分析工具JConsole:Java 监视与管理控制台连接…

司徒理财:8.17黄金反弹遇阻,1900现价空!

黄金趋势下跌&#xff0c;现在反弹遇阻&#xff0c;继续空&#xff01;除非行情强势站上1907位置&#xff0c;否则还是空头下跌走势&#xff0c;反弹遇阻直接空&#xff01;      黄金从走势上看&#xff0c;一直阴跌&#xff0c;并且在昨日加速下行&#xff01;现在黄金受…