<Linux> 初识线程

news2024/12/29 11:34:03

目录

前言:

一、什么是线程

(一)基本概念

(二)线程理解

(三)线程与进程的关系

(四)简单实用线程

(五)重谈虚拟地址空间

1. 页表的大小

2. 内存与磁盘的交互

3. 局部性原理

4. 页表映射原理

5. 扩展:动态内存管理

(六)小结

二、线程优缺点

(一)优点

(二)缺点

(三)线程用途


前言:

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

Windows11中的线程:

一、什么是线程

(一)基本概念

教材观点:

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

内核观点:

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

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

(二)线程理解

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

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

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

进程之间是相互独立

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

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

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

为了避免这种繁琐的操作,引入了 线程 的概念,所谓线程就是:额外创建一个 task_struct 结构,该 task_struct 同样指向当前的虚拟地址空间,并且不需要建立映射关系及加载代码和数据,如此一来,我们就会发现我们新创建的task_struct 好像也是一个"进程",更是一个独立的执行流,其实这就是创建一个线程。操作系统只需要针对一个 task_struct 结构即可完成调度,成本非常低。

为什么线程其执行粒度比进程更细?

现在一个进程有多个task_struct ,都指向同一个地址空间。把地址空间中例如代码区的代码划分成多个部分,每一个 task_struct 在未来执行时,都执行同一个地址空间的不同部分的代码。这样一个进程中就存在了多个执行流(线程)。因此线程是一个执行分支,这样每个线程就能更精确的执行完成任务。

为什么调度线程的成本比进程更低?

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

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

进程processtask_struct 称为 PCB线程(thread)task_struct 则称为 TCB

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

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

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

(三)线程与进程的关系

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

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

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

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

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

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

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

在 Linux 中,认为 PCB TCB 的共同点太多了,于是直接复用了 PCB 的设计思想和调度策略,在进行 线程管理 时,完全可以复用 进程管理 的解决方案(代码和结构),这可以大大减少系统调度时的开销,做到 小而美,因此 Linux 中实际是没有真正的 线程 概念的有的只是复用 PCB 设计思想的 TCB。在这种设计思想下,线程 注定不会过于庞大,因此 Linux 中的 线程 又可以称为 轻量级进程(LWP轻量级进程 足够简单,且 易于维护、效率更高、安全性更强,可以使得 Linux 系统不间断的运行程序,不会轻易 崩溃。

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

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

(四)简单实用线程

接下来简单使用一下 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 指明使用 线程原生库:

g++ -o $@ $^ -std=c++11 -lpthread

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

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

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

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

可以看到此时有 四个线程

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

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

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

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

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

(五)重谈虚拟地址空间

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

1. 页表的大小

页表 是用来将 虚拟地址 和 物理地址 之间建立映射关系的,页表难道真的只是简单一 一存储映射吗?除此之外,页表 中还存在 其他属性 字段

在 32 位系统中,存在 2^32 个地址(一个内存单元大小是 1byte),虚拟地址空间中的每一个地址依次为 [0, 2^32 − 1] 0x00000000 - 0xFFFFFFFF,也就是我们常说的 4 GB 虚拟内存空间。

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

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

2. 内存与磁盘的交互

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

通常 IO 的数据以  为基本单位,在文件系统中,一个  的大小为 4KB(一个块由8个扇区组成,单个扇区大小为 512Byte),所以OS在和磁盘这样的设备进行IO交互的时候,就不能按照字节为单位的而是要按照为单位。即使我们一次只想获取一个字节,操作系统最低也会 IO 一个 数据块4KB)。

4KB 这个大小很关键

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

为了让内存与与磁盘更高效地进行IO,操作系统对内存也进行了按管理划分,OS将内存划分成一个个页框,其中每个页框可以存储的数据的大小为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,那么等分为 4KB 的 页 Page,可以得到约 100w 个 页 Page,其中 struct page 结构体不会设计的很大,大小是 字节 级别的,也就是说 struct page mem[100w] 占用的总大小不过 4~5MB,对于偌大的内存来说可以忽略不计。

内存管理的本质:

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

3. 局部性原理

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

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

局部性原理的特征

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

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

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

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

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

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

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

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

4. 页表映射原理

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

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

  • 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 大小,即可映射至每一个 物理内存,但实际上 物理内存 并不会被时刻占满,大多数情况下都是使用一部分,因此实际页表大小不过 几十字节。

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

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

5. 扩展:动态内存管理

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

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

具体原理: 

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

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

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

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

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

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

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

比如这段代码:

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

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

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

程序运行后,就会报错:

(六)小结

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

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

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

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

二、线程优缺点

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

线程 有以下概念:

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

(一)优点

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

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

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

(二)缺点

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

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

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

#include <iostream>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void *threadHandler1(void *args)
{
    while (true)
    {
        cout << "我是次线程1,我正在运行..." << endl;
        sleep(1);
    }
}

void *threadHandler4(void *args)
{
    while (true)
    {
        sleep(5); // 等其他线程先跑一会
        cout << "我是次线程4,我正在运行..." << endl;
        char *ps = "good morning";
        *ps = 'A';
    }
}

int main()
{
    pthread_t t1, t4; // 创建两个线程
    pthread_create(&t1, NULL, threadHandler1, NULL);
    pthread_create(&t4, NULL, threadHandler4, NULL);

    // 主线程运行
    while (true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }

    return 0;
}

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

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

  • 站在技术角度:完全可以让其自行承担,但这不合理
  • 系统角度:线程是进程的执行分支,线程出问题了,进程也不应该继续运行(比如一颗老鼠屎坏了一锅汤)
  • 信号角度:线程出现异常后,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、编程难度提高,编写与调试一个多线程程序需要考虑许多问题,诸如 加锁、同步、互斥 的等,面对多个执行流时,调试也是非常困难的。

(三)线程用途

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

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

三、

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

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

相关文章

信息安全、网络安全以及数据安全三者之间的区别

随着信息技术的飞速发展&#xff0c;网络安全、信息安全、数据安全等词汇在平时出现的频率越来越高&#xff0c;尤其是数据安全&#xff0c;是大家都关心的一个重要话题。事实上&#xff0c;有很多人对网络安全、信息安全、数据安全的概念是区分不清的&#xff0c;下面由我帮大…

Yolov8-pose关键点检测:特征融合涨点篇 | 广义高效层聚合网络(GELAN) | YOLOv9

💡💡💡本文独家改进:即结合用梯度路径规划(CSPNet)和(ELAN)设计了一种广义的高效层聚合网络(GELAN),高效结合YOLOv8-pose,实现涨点。 将GELAN添加在backbone和head处,提供多个yaml改进方法 Yolov8-Pose关键点检测专栏介绍:https://blog.csdn.net/m0_6377421…

二维数组的传递和返回

指针和二维数组 指针存储的是内存单元的地址&#xff0c;当使用引用运算符 *&#xff0c;或者变址运算符 [ ] 时才能将指针所指向的内存单元中的值取出。 指针有两个关键属性&#xff1a; 1.它存储的是内存地址 2.它存储的是什么类型变量的内存地址&#xff0c;这一点非常…

盛元广通粮油质量检测实验室管理系统

近年来对于食品安全问题层出不穷&#xff0c;为提高粮食检测中心管理水平&#xff0c;关系到千千万万的消费者的健康饮食问题&#xff0c;粮油作为老百姓日常生活饮食必需品、消耗品&#xff0c;需从源头上对粮食在本省&#xff08;区、市、县&#xff09;不同粮食品种检测检测…

vulhub靶场-matrix-breakout-2-morpheus

下载&部署 从官网中下载 https://www.vulnhub.com/entry/matrix-breakout-2-morpheus,757/ 下载完成后&#xff0c;在vmware中打开&#xff0c;选择刚刚下载的ova文件 vmware打开文件后需要将刚导入的机器重新启动 再检查下网卡是否是和kali在同一张网卡下就可以开始打靶了…

使用Amazon Bedrock托管的Claude3 学习中国历史

最近被Amazon Bedrock托管的Claude3 刷屏了&#xff0c;那么先简单介绍下什么是Claude 3。 Claude 3是Anthropic 推出了下一代 Claude模型&#xff0c;针对不同用例进行优化的三种先进模型&#xff1a;Claude 3 Haiku、Claude 3 Sonnet 和 Claude 3 Opus&#xff0c;使用户能够…

【Hadoop大数据技术】——HDFS分布式文件系统(学习笔记)

&#x1f4d6; 前言&#xff1a;Hadoop的核心是HDFS&#xff08;Hadoop Distributed File System&#xff0c;Hadoop分布式文件系统&#xff09;和MapReduce。其中&#xff0c;HDFS是解决海量大数据文件存储的问题&#xff0c;是目前应用最广泛的分布式文件系统。 目录 &#x…

【SpringCloud微服务实战03】Nacos 注册中心

一、Nacos安装 官方文档安装Nacos教程:Nacos 快速开始 这里安装的是1.4.7版本,安装之后访问http://127.0.0.1:8848/nacos 管理界面如下:(用户名:nacos,密码:nacos) 二、Nacos服务注册和发现 1、在父工程中配置文件pom.xml 中添加spring-cloud-alilbaba的管理依赖:…

模型分析与偏差和方差

在创建一个机器学习系统&#xff0c;当我们的模型出现问题时&#xff0c;我们需要去找到最优的方式&#xff0c;能解决我们的问题&#xff0c;这时我们就需要会去诊断问题。 模型评估(Evaluating a model)&#xff1a; 1.训练集和测试集判断&#xff1a; 我们一般把数据组的前…

LeetCode - 寻找数组的中心

先学习一下前缀和吧 LCR 012.寻找数组的中心LCR 012. 代码解析 在读题读到左侧元素之和等于右侧所有元素之和的时候&#xff0c;我觉得可以用前缀和&#xff0c;然后结合下面的示例&#xff0c;模拟了一下发现确实可以。 我的想法是搞两个数组&#xff0c;一个来存从左到右数…

MySQL索引+常见问题详解

文章目录 字典的数据部分字典的索引部分数据页索引页MySQL单表最大数据容量2000万&#xff1f;MySQL为什么采用B树&#xff1f;聚簇索引 / 非聚簇索引回表 / 索引覆盖为什么建议自增主键索引下推 网络上的讲述MySQL索引的文章太多了&#xff0c;我打算换个角度来说。我们尝试着…

物奇平台超距断连无蓝牙广播问题解决方法

是否需要申请加入数字音频系统研究开发交流答疑群(课题组)?可加我微信hezkz17, 本群提供音频技术答疑服务,+群赠送语音信号处理降噪算法,蓝牙耳机音频,DSP音频项目核心开发资料, 物奇平台超距断连无蓝牙广播问题解决方法 一 问题反馈 二解决方法: 1 运行流程分析 对应代…

八、软考-系统架构设计师笔记-系统质量属性和架构评估

1、软件系统质量属性 软件架构的定义 软件架构是指在一定的设计原则基础上&#xff0c;从不同角度对组成系统的各部分进行搭配和安排&#xff0c;形成系统的多个结构而组成架构&#xff0c;它包括该系统的各个构件&#xff0c;构件的外部可见属性及构件之间的相互关系。 软件架…

盘点2024年2月Sui生态发展,了解Sui近期成长历程!

2024年2月喜迎中国春节&#xff0c;Sui生态发布多条重要公告&#xff0c;生态建设积极推进。例如&#xff0c;Sui上TVL突破5亿美元跻身DeFi榜单前十名和最活跃链前五名&#xff1b;Ondo宣布将其原生稳定币USDY带入Sui生态&#xff1b;Sui与Thrive合作推出ThinkSui平台&#xff…

2024年品牌推广:构建品牌生态圈与注重品牌故事和文化传播

在全球经济深度融合、数字化浪潮汹涌澎湃的2024年&#xff0c;品牌推广的策略与模式正经历着前所未有的变革。在这一背景下&#xff0c;构建品牌生态圈和注重品牌故事与文化传播&#xff0c;成为了企业提升品牌竞争力和市场占有率的重要手段。 一、2024年市场经济分析与现状 …

安装YOLOv8

1.创建、激活虚拟环境 conda create --name yolov8 python conda activate yolov8 2.安装YOLOv8 pip install ultralytics 3.在官方网页中下载YoloV8的所有的代码、并且解压&#xff1a; 官方网页&#xff1a;https://github.com/ultralytics/ultralytics/ultralytics 4.进入…

聚道云软件连接器3月新增应用/产品更新合集

3月更新概要 新增应用&#xff1a; 应用1&#xff1a;华为云welink 应用2&#xff1a;易宝支付 应用3&#xff1a;励销云CRM 应用4&#xff1a;分贝通 应用5&#xff1a;灵当CRM 新增&更新功能 1、【流程】中增加流程树状管理 新增应用 应用1&#xff1a;华为云wel…

全面认识计算机操作系统(二)

目录 一、操作系统的诞生 相关概念&#xff1a; 1. 手工操作阶段 2. 脱机输入 / 输出阶段 &#xff08;1&#xff09;脱机输入技术 &#xff08;2&#xff09;脱机输出技术 3. 单道批处理阶段 4. 多道批处理阶段 5. 分时技术产生 6. 实时系统产生 二、现代操作系统的…

学校里的软件测试专业技能到底怎样,为什么应届生都很难找工作?

大家好&#xff0c;今天和以为在学校教软件测试的老师聊了天&#xff0c;主要聊的主题是-为什么现在大专生就业这么困难。主要总结以下几点&#xff01; 一是受当下大环境的影响&#xff08;比如疫情、俄乌、单边主义等&#xff09;&#xff1b;二是今年的应届生实在太多&…

客户案例|100M 768 维向量数据,Zilliz Cloud 稳定支持 Shulex VOC 业

日前&#xff0c;国际化 VOC SaaS 公司数里行间&#xff08;Shulex&#xff09;将上亿数据量的核心业务从开源向量数据库 Milvus 迁移至全托管的向量数据库云服务 Zilliz Cloud。 相比于 Milvus&#xff0c;Zilliz Cloud 实现了 Shulex VOC 评论分析洞察报告生成速度 30% 的提升…