【Linux】Linux下多线程

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

文章目录

  • 1. 前置:进程地址空间和页表
    • 1.1 如何看待进程地址空间和页表
    • 1.2 虚拟地址和物理地址之间的转换
  • 2. 线程的理解
    • 2.1 线程概念
    • 2.2 Linux下线程的实现
    • 2.3 线程的优缺点&&线程的用途&&线程异常
    • 2.4 Linux进程vs线程
      • 2.4.1 进程和线程
      • 2.4.2 进程内多线程
      • 2.4.3 进程和线程的关系
  • 3. 线程控制
    • 3.1 POSIX线程库
    • 3.2 线程创建
    • 3.3 线程终止
      • 3.3.1 线程函数return
      • 3.3.2 pthread_ exit
      • 3.3.3 pthread_ cancel
    • 3.4 线程id && 线程在进程地址空间内布局
    • 3.5 线程等待
    • 3.5 分离线程
  • 4. 原生线程库的二次封装

1. 前置:进程地址空间和页表

1.1 如何看待进程地址空间和页表

我们之前了解到的进程的概念,对于每个进程,有一个PCB对象,对应的虚拟进程地址空间,对应的用户级页表

进程所有能看到的资源都包括在其中了,所以我们可以有以下结论:

1. 进程地址空间是进程能看到的资源窗口

2. 页表决定了进程真正拥有的资源

3. 合理的利用进程地址空间+页表能够将进程拥有的资源进行分类

1.2 虚拟地址和物理地址之间的转换

要知道虚拟地址和物理地址之间的转换,就得先明确映射结构,也就是页表的结构

首先页表中除了物理地址和虚拟地址之外,每个地址项还有一些其他的内容:

image-20240127145112702

存储内容的解释:

  • 是否命中:如果在访问资源的时候,目标资源不在内存中,就会触发缺页中断,OS将目标资源加载到物理内存,建立映射

  • RWX权限:举个例子:我们在C语言中写出这样的语句char *str = "hello world";*str = 'H',在运行时就会报错,这是因为用户没有对str指向的空间的写权限,这里对指定空间的读写权限就是存放在此位置

  • UK权限:这里的UK指的就是User和Kernel,有些地址是用户级的,有些是内核级的,这里用UK权限做区分

在之前的认知里面,页表就是一个KV的映射结构,一个虚拟地址对应一个物理地址的映射表格。但是如果再想细一点,如果一个虚拟地址的内存单元对应一个物理地址,那么理论上(不考虑用户级页表和内核级页表的区分),一个页表在32位机器下,最大的大小就是 4G个内存单位 * (一个物理地址大小 + 一个虚拟地址大小)。这太大了,如果想要保存一个页表,就需要非常大的内存空间,这显然是不合理的。所以实际上页表并不是一个单纯的映射表格结构。

多级页表结构

在32位机器下,地址有32个比特位,这里我们将这个32个比特位分成了3个部分:前10位,中间10位,后12位

其中前10位表示一级页表的页表中查找,我们把一级页表叫做页目录,中间10位在二级页表中查找,我们把二级页表叫做页表,找到对应的物理内存的起始地址,后12位表示在对应的物理内存起始地址的偏移量,12位刚好表示212B,也就是4KB,这也就是为什么物理内存在从磁盘上加载数据的单位是4KB,我们把4KB称为一个页帧

img

映射过程,由MMU这个硬件完成的,该硬件是集成在CPU内的,页表是一种软件映射,MMU是一种硬件映射,虚拟地址转成物理地址实际上是软硬件结合的

拓展:当然物理内存也是需要管理起来的,所以也需要先描述再组织。其中物理内存的管理算法是伙伴算法,感兴趣的可以自行搜索研究

2. 线程的理解

2.1 线程概念

什么是线程?

在操作系统的教材里面的解释是:线程是一个进程内部的控制序列。这句话挺难理解,我们换一个说法

在进程信号这篇文章中我们讲到过可重入函数的概念,其中提到了执行流的概念,所谓的执行流就是一个执行顺序。控制序列也就是这个执行流

一个进程至少有一个执行流

线程在进程内运行,本质就是在进程地址空间内运行

这里我们引入了一个新的概念:线程。线程在OS内部也是需要被管理起来的,既然要做管理,就要先描述再组织

事实上有操作系统就是这么做的,比如Windows就是真正在OS内部实现了

但是在Linux系统内核中,是没有线程这个概念的,Linux下只有进程的概念。

  • 那我们说的线程在Linux下是什么呢?

在Linux下,”线程“是CPU调度的基本单位

  • 如何看待我们之前学习进程时对应的进程的概念,和现在说的不冲突吗?

首先我们给进程重新下一个定义,在内核的视角,进程就是承担分配的系统资源的基本实体, 包括内核数据结构和进程对应的代码与数据。我们之前讲的进程可以理解成内部只有一个执行流的进程。但是一个进程内部实际上可以有多个执行流


上面我们说线程在Linux下是CPU调度的基本单位,是怎么实现的呢?

在内核的角度下,OS不关心线程和进程的概念区分,OS看到的就是进程PCB。实际上在Linux下一个进程可以有多个task_struct,我们称之为轻量级进程至此我们明白了,在Linux内核视角下,只有轻量级进程的概念,没有线程的概念

image-20240128185339528

2.2 Linux下线程的实现

  • **如果OS真的要专门设计“线程”这个概念,那么未来OS要不要对线程做管理呢?**显而易见,当然是要的

  • 那OS要怎么管理线程呢? 先描述再组织

  • 怎么描述? 需要定义一些属性,描述现成的被执行情况和被调度情况(id,上下文,状态,栈…)

单纯从调度的角度来说,线程调度和进程调度有很多重合的地方,所以Linux在开发的时候,就没有单独设计对应的线程数据结构,而是复用了进程的task_struct,用其表示“线程”。


接下来我们来明确几个共识和理解:

1. Linux内核中没有真正意义上的线程,Linux是用进程PCB来模拟线程的,是一套完全属于自己的线程解决方案

2. 站在CPU的视角,CPU看不到进程和线程的分别,每一个PCB都可以看作是一个“轻量级进程”

3. Linux下线程是CPU调度的基本单位,进程是承担系统分配资源的基本实体

4. Linux下进程用来向OS申请资源,线程伸手向进程要资源

5. Linux下没有真正意义上的线程的优点是:代码结构简单,好维护,可靠。缺点是OS只认线程,程序员(用户)只认线程,但是Linux没办法直接给我们提供创建线程的系统调用接口,只能提供创建轻量级进程的系统调用接口

Linux下创建轻量级进程有很多系统调用,最典型的就是vfork,vfork和fork的用法基本一致,只是vfork创建的进程(轻量级线程)和父进程共享进程地址空间(mm_struct)。

image-20240128221019971

见一见“猪跑”

image-20240128222222921

2.3 线程的优缺点&&线程的用途&&线程异常

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

  • 性能损失
    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
    线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
    同步和调度开销,而可用的资源不变。

  • 健壮性降低
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
    不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制

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

  • 编程难度提高

    ​ 编写与调试一个多线程程序比单线程程序困难得多

线程异常

当线程发生异常的时候,OS会向这个线程所在的进程发送信号,这个进程就会退出,而不是结束一个线程

线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率

  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

2.4 Linux进程vs线程

2.4.1 进程和线程

  • 线程是CPU调度的基本单位;进程是承担OS分配资源的基本实体
  • 进程具有独立性,相互通信难度较高;同一进程内的线程共享绝大多数数据

虽然同一进程内多个线程的数据大多是共享的,但是也有一些私有的数据

  • 线程ID
  • 一组寄存器(上下文)
  • errno
  • 信号屏蔽字(block位图结构)
  • 调度优先级

2.4.2 进程内多线程

进程内多线程共享同一地址空间,因此代码段(Text Segment)、数据段(Data Segment)都是共享的

  • 如果定义一个函数,在各线程中都可以调用;
  • 如果定义一个全局变量,在各线程中都可以访问到
  • 除此之外,各线程还共享以下进程资源和环境:
    • 文件描述符表
    • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
    • 当前工作目录
    • 用户id和组id

2.4.3 进程和线程的关系

image-20240128224511137

我们之前学习过程中的单进程实际上就是具有一个线程执行流的进程

3. 线程控制

3.1 POSIX线程库

在上面的内容中,我们有了两个共识:

  1. Linux下没有真正意义上的线程,没有办法直接提供线程控制的系统调用,它只认轻量级进程
  2. OS和程序员(用户)只认线程,要操作线程

所以就会出现冲突,程序员和系统没有办法交互了?!!

所以在用户层和内核层之间,Linux提供了一个库,就是我们说的原生的用户级线程库pthread

在任意一个Linux系统下的lib64目录下都能找到

image-20240128225655451

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的 要使用这些函数库,要通过引入头文件<pthread.h> 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

3.2 线程创建

pthread_create

image-20240128231039850

函数描述:创建一个新线程
头文件:
#include <pthread.h>
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数解释:thread:输出型参数,向其中填充线程id返回给主线程attr:设置线程的属性,attr为NULL表示使用默认属性start_routine:是一个函数指针(回调函数),表示新线程启动后要执行的函数arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

关于多线程的错误检查的补充

  • 在我们使用传统的函数的时候,都会通过返回值表示函数执行情况,使用全局的错误码errno表示错误类型
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

实例:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *start_routine(void *arg)
{while (true){std::cout << "我是" << (char *)arg << "我正在运行" << std::endl;sleep(1);}
}
int main()
{pthread_t id = 0;pthread_create(&id, NULL, start_routine, (void *)"thread 1");while(true){std::cout << "我是主线程,我正在运行..." << std::endl;sleep(1);}return 0;
}

Linux下查看当前线程ps -aL

写一个监测脚本查看指定线程的情况

while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1; done

image-20240128233443920

这里的LWP表示的就是light weight process,轻量级进程。CPU在进行调度的时候,对每个进程的识别依靠的就是LWP

  • 我们之前说的CPU在调度的时候调度的是进程id和这句话冲突吗?

    当然不冲突,对于任意一个进程,其中都会存在一个主线程,这个主线程的LWP和PID是一样的。同一进程中所有线程的PID都是相同的,CUP调度使用LWP来区分不同线程

3.3 线程终止

我们知道,线程是进程内部的一个执行流,当一个进程被终止,其内部的所有线程都将被终止,但是如果想要只终止一个线程,不影响其他线程的话,有三种方法。

3.3.1 线程函数return

这种方法对主线程不适用,因为从main函数return相当于调用exit直接终止整个进程

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>void *start_routine(void *arg)
{int cnt = 5;while (true){std::cout << "我是" << (char *)arg << "我正在运行" << std::endl;if(cnt-- == 0) break;sleep(1);}return nullptr;
}int main()
{pthread_t id = 0;pthread_create(&id, NULL, start_routine, (void *)"thread 1");while(true){std::cout << "我是主线程,我正在运行..." << std::endl;sleep(1);}return 0;
}

image-20240128234451881

3.3.2 pthread_ exit

线程可以调用pthread_ exit终止自己

image-20240128234541570

函数描述:终止调用这个函数的线程
头文件:
#include <pthread.h>
函数原型:
void pthread_exit(void *retval);
参数解释:retval:保存该线程退出的时候返回的值的地址,其指向的内存单元必须是全局的或者在堆上的
返回值: 这个函数无返回值
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *start_routine(void *arg)
{int cnt = 5;while (true){std::cout << "我是" << (char *)arg << "我正在运行 " << cnt << std::endl;if(cnt-- == 0){std::cout << "新线程退出..." << std::endl;pthread_exit(NULL);}sleep(1);}return nullptr;
}int main()
{pthread_t id = 0;pthread_create(&id, NULL, start_routine, (void *)"thread 1");while(true){std::cout << "我是主线程,我正在运行..." << std::endl;sleep(1);}return 0;
}

image-20240129140450794

3.3.3 pthread_ cancel

一个线程可以调用pthread_ cancel终止同一进程中的另一个线程

image-20240129140600209

头文件:
#include <pthread.h>
函数原型:
int pthread_cancel(pthread_t thread);
函数描述:发送一个取消请求给指定线程,终止该线程
参数解释:thread:要取消的线程
返回值:调用成功返回0,否则返回一个非0的错误码

3.4 线程id && 线程在进程地址空间内布局

我们知道,使用pthread_create函数可以创建一个进程,同时产生一个pthreat_t类型的数据,以输出型参数的方式返回给主线程,也就是线程id,这个id是什么呢?

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <pthread.h>
void *start_routine(void *arg)
{while (true){std::cout << "我是" << (char *)arg << "我正在运行 " << std::endl;printf("我是%s我正在运行\n", (char *)arg);sleep(1);}return nullptr;
}
int main()
{pthread_t id = 0;pthread_create(&id, NULL, start_routine, (void *)"thread 1");while (true){printf("我是主线程,新线程的id是%d 0x%x\n", id, id);sleep(1);}return 0;
}

image-20240129165127549

按照十进制和十六进制分别输出,这个东西看起来像是一个地址。那么它到底是什么呢?

这个id是什么取决于不同的实现,对于Linux目前实现的NPTL(Native POSIX Thread Library)而言,pthread_t类型的线程ID本质上是进程地址空间中的一个地址

我们之前说同一进程的线程之间绝大多数的的资源是共享的,但是还有一些资源是自己私有的。每个线程都有自己独立的栈,主线程的栈是进程地址空间中原生的栈,其他线程采用的是共享区中的栈。

我们知道Linux内核是没有线程这个概念的,线程的实现是依赖于原生线程库pthread,所以pthread中需要对线程做管理,所以在这个动态库中就定义了struct pthread结构体。

线程中私有的资源包括三个部分:struct pthread,线程局部存储,线程栈

image-20240129172241462

线程函数起始是在库内部对线程属性进行操作,最后将要执行的代码交给对应的内核级LWP去执行。所以线程数据的管理本质在共享区


我们知道在主线程中,调用pthread_create的时候可以在主线程中拿到线程id,那么在新线程中怎么样能够找到自己的线程id呢?

通过函数pthread_self

image-20240129172537571

头文件:#include <pthread.h>
函数原型:pthread_t pthread_self(void);
函数描述:获取当前线程的线程id
返回值:返回当前线程的线程id
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <pthread.h>
void *start_routine(void *arg)
{while (true){pthread_t id = pthread_self();printf("我是%s我正在运行,线程id是0x%x\n", (char *)arg, id);sleep(1);}return nullptr;
}
int main()
{pthread_t id = 0;pthread_create(&id, NULL, start_routine, (void *)"thread 1");while (true){printf("我是主线程,新线程的id是%d 0x%x\n", id, id);sleep(1);}return 0;
}

image-20240129173053674

局部性存储的验证:

#include <iostream>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <pthread.h>int g_val = 100;void *start_routine(void *arg)
{std::string name = static_cast<const char *>(arg);while (true){std::cout << name << " running ... "<< "g_val: " << g_val << "&g_val: " << &g_val << std::endl;sleep(1);++g_val;}return nullptr;
}
int main()
{pthread_t id = 0;pthread_create(&id, NULL, start_routine, (void *)"thread 1");while (true){printf("main thread g_val: %d &g_val: 0x%x\n", g_val, &g_val);sleep(1);}return 0;
}

image-20240129173635239

但是在g_val前面加上__thread就可以让全局变量在各个线程内私有

__thread int g_val = 100;

image-20240129174005583

3.5 线程等待

进程需要被等待,防止出现僵尸进程的情况,导致资源泄漏,当然线程也是需要被等待的,这是因为

  • 已经退出的线程,其私有空间没有被释放,仍然在进程的地址空间内
  • 创建的新线程不会附庸刚才退出的现成的地址空间

使用pthread_join函数来进行线程的阻塞式等待

image-20240129174516234

头文件:#include <pthread.h>
函数原型:int pthread_join(pthread_t thread, void **retval);
函数描述:参数解释:thread:要被等待的线程idretval:指向一个存放线程返回值的指针,(线程返回值的类型是void*)要保存这个结果的输出型参数的类型是void**
返回值:成功返回0;失败返回错误码

在前面我们说到线程退出的三种方式,实际上三种退出方式退出的线程,使用pthread_join得到的终止状态是不同的

  • 如果是通过return退出的,retval所指向的内存单元存放的是线程调用函数的返回值即start_routine函数的返回值
  • 如果是被别的进程调用pthread_cancel异常终止的话,retval所指向的内存单元存放的是常数PTHREAD_CANCELED
  • 如果是自己调用pthread_exit终止的话,retval所指向的内存单元存放的是传给pthread_eixt的参数
  • 如果不关心线程的终止状态,直接传NULL即可

image-20240129180752207

线程整个生命周期的时间线:

image-20240129163451342

3.5 分离线程

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

告诉线程的方式就是通过函数pthread_detach

image-20240129180953872

头文件:#include <pthread.h>
函数原型:int pthread_detach(pthread_t thread);
参数解释:thread:要分离的线程id
函数描述:将指定线程进行线程分离,告诉OS线程退出只会自动释放线程资源
返回值:调用成功返回0,否则返回错误码

注意:

  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的
#include <iostream>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <pthread.h>void *start_routine(void *arg)
{pthread_detach(pthread_self());printf("%s\n", (char *)arg);return NULL;
}
int main()
{pthread_t tid;if (pthread_create(&tid, NULL, start_routine, (void *)"thread 1") != 0){printf("create thread error\n");return 1;}sleep(1); // 注意这里一定要让线程先分离,再等待,因为我们不能保证主线程和新线程谁先执行if (pthread_join(tid, NULL) == 0){printf("pthread wait success\n");}else{printf("pthread wait failed\n");}return 0;
}

image-20240129181555405

4. 原生线程库的二次封装

原生线程库是C语言实现的,所有语言如果想使用多线程,在底层都需要调用这个原生线程库pthread

那么现在我们可以来实现一个C++版本的线程库

库代码实现:

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cassert>class Thread;class Context
{
public:Thread *this_;void *args_;public:Context(Thread *thread = nullptr, void *args = nullptr): this_(thread), args_(args){}~Context() {}
};class Thread
{
public:using func_t = std::function<void *(void *)>; // 定义func_t类型static int number;                            // 线程编号,按照一次运行时的调用次数计数
public:Thread(func_t func, void *args) : func_(func), args_(args){char *buffer = new char[64];name_ = "thread-" + std::to_string(++number);}// 问题2:这里如果实现成普通的类内方法,还是会报错,这是因为类内方法默认传了this指针// 解决方案:可以把这个函数设计成static的或者在类外构建,然后在类内声明成友元函数,这里采用static的方式static void *start_routine(void *args){// 问题3:static的函数没有办法访问到类成员变量,就没有办法调用func_和args// 解决方案:通过args传入线程运行时所需要的全部内容(构造成一个对象传入)ContextContext *ctx = static_cast<Context *>(args);// 问题4:在static函数内无法访问类内私有成员,也就没有办法调用func_方法,所以在类内封装一个run函数用于调用func_void *ret = ctx->this_->run(ctx->args_);delete ctx;return ret;}void *run(void *arg){return func_(arg);}void start(){// 问题1:这里如果直接传func_发现会报错,这是因为func_的类型是 std::function<void *(void *)>,但是这里需要的参数类型是void*(*)(void*)// 解决方案:在类里面实现一个void*(*)(void*)类型的函数,在这个函数中调用func_Context *ctx = new Context(this, args_);int n = pthread_create(&tid_, nullptr, start_routine, ctx);assert(n == 0);(void)n;}void join(){int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;}~Thread() {}private:std::string name_; // 线程名pthread_t tid_;    // 线程idfunc_t func_;      // 线程调用的函数void *args_;       // 线程调用函数的参数
};
int Thread::number = 0;

测试代码:

#include "Thread.hpp"
#include <unistd.h>void *thread_run(void *args)
{std::string arg = static_cast<const char*>(args);while (true){std::cout << arg << std::endl;sleep(1);}return nullptr;
}
int main()
{Thread *thread1 = new Thread(thread_run, (void *)"thread 1");Thread *thread2 = new Thread(thread_run, (void *)"thread 2");Thread *thread3 = new Thread(thread_run, (void *)"thread 3");thread1->start();thread2->start();thread3->start();thread1->join();thread2->join();thread3->join();return 0;
}

image-20240129224421221


本节完…

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

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

相关文章

MIAOYUN获评OpenCloudOS社区2023年度优秀贡献企业

近日&#xff0c;OpenCloudOS社区发布了社区年度贡献企业榜单&#xff0c;成都元来云志科技有限公司&#xff08;简称“MIAOYUN”&#xff09;凭借对国产开源的热情&#xff0c;及对操作系统产业的支持&#xff0c;通过兼容适配互认证&#xff0c;为推动OpenCloudOS规模化应用&…

使用IP爬虫代理提取数据的步骤是什么?爬虫代理IP怎么提高采集效率?

​​​​​ 一、使用IP爬虫代理提取数据的步骤 在使用爬虫代理IP提取数据之前&#xff0c;需要先了解数据来源和目标网站的结构。以下是一个基本的步骤&#xff1a;1.确定数据来源 首先需要确定要提取数据的网站或数据源&#xff0c;了解网站的结构、数据存储方式以及数据更新…

Qt应用软件【协议篇】Modbus详细介绍

文章目录 概述Modbus 485协议概述在工业自动化中的应用 通信模式数据模型功能码 Modbus RTU示例 1&#xff1a;读取保持寄存器示例 2&#xff1a;写入单个线圈示例 3&#xff1a;写入多个保持寄存器报文传输 Modbus ASCII报文示例报文传输 Modbus TCP报文示例报文传输 概述 在…

如何将iPad连接到USB设备?这里提供了详细步骤

本文介绍了如何将iPad连接到USB设备。说明适用于所有版本的iPad。 将USB设备与带USB-C端口的iPad一起使用 以下iPad具有USB-C端口: 自2018年第三代以来的iPad Pro机型 自2020年第四代以来的iPad Air机型 自2021年第六代以来的iPad迷你机型 自2022年以来的第十代iPad机型 这些…

动手学深度学习(一)深度学习介绍2

目录 二、起源 三、深度学习的成功案例&#xff1a; 四、特点&#xff1a; 五、小结&#xff1a; 二、起源 为了解决各种各样的机器学习问题&#xff0c;深度学习提供了强大的工具。 虽然许多深度学习方法都是最近才有重大突破&#xff0c;但使用数据和神经网络编程的核心思…

Redis学习——高级篇①

Redis学习——高级篇① Redis7高级之单线程和多线程&#xff08;一&#xff09; 一、Redis单线程VS多线程1.Redis的单线程部分1.1 Redis为什么是单线程&#xff1f;1.2 Redis所谓的“单线程”1.3 Redis演进变化1.3.1 Redis 3.x 单线程时代性能很快的原因1.3.2…

【Linux网络编程】网络编程套接字(1)

【Linux网络编程】网络编程套接字(1) 目录 【Linux网络编程】网络编程套接字(1)源IP地址和目的IP地址端口号端口号和进程ID的关系 网络通信TCP协议UDP协议网络字节序socket编程接口简单的UDP网络程序 作者&#xff1a;爱写代码的刚子 时间&#xff1a;2024.1.29 前言&#xff1…

树--二叉树(C语言纯手凹)

目录 目录 1.什么是树&#xff1f;&#xff08;不深入&#xff0c;仅做了解&#xff09; 2.树的表示方式 2.1孩子兄弟表示法&#xff08;左孩子右兄弟&#xff09; 2.2孩子表示法 2.3双亲表示法 3.什么是二叉树 4.二叉树分类 4.1满二叉树 4.2完全二叉树 4.3二叉搜索树…

基础小白快速学习c语言----变量的仔细介绍

变量&#xff1a; 表面理解&#xff1a;在程序运行期间&#xff0c;可以改变数值的数据&#xff0c; 深层次含义&#xff1a;变量实质上代表了一块儿内存区域&#xff0c;我们可以将变量理解为一块儿内存区域的标识&#xff0c;当我们操作变量时&#xff0c;相当于操作了变量…

qemu搭建arm64 linux kernel环境

一、环境准备 ubuntu 22.04 内核源码&#xff1a;linux-6.6.1 &#xff08;直接上最新版&#xff09; 下载链接&#xff1a;The Linux Kernel Archives 交叉编译工具链&#xff1a; sudo apt-get install gcc-12-aarch64-linux-gnu 具体能用的版本gcc-XX-arch64-linux-gnu…

如何使用IaC Scan Runner扫描IaC中的常见安全漏洞

关于IaC Scan Runner IaC Scan Runner是一款针对IaC&#xff08;基础设施即代码&#xff09;的安全漏洞扫描工具&#xff0c;在该工具的帮助下&#xff0c;广大安全开发人员可以轻松扫描IaC&#xff08;基础设施即代码&#xff09;中的常见漏洞。 IaC Scan Runner本质上是一个…

正则表达式 文本三剑客

一 正则表达式&#xff1a; 由一类特殊字符及文本字符所编写的模式&#xff0c;其中有些字符&#xff08;元字符&#xff09;不表示字符字面意义&#xff0c;而表示控制或通配的功能&#xff0c;类似于增强版的通配符功能&#xff0c;但与通配符不同&#xff0c;通配符功能是用…

2023年算法GWCA -CNN-BiLSTM-ATTENTION回归预测(matlab)

2023年算法GWCA -CNN-BiLSTM-ATTENTION回归预测&#xff08;matlab&#xff09; GWCA -CNN-BiLSTM-Attention长城建造算法优化卷积-长短期记忆神经网络结合注意力机制的数据回归预测 Matlab语言。 长城建造算法&#xff08;Great Wall Construction Algorithm&#xff0c;GWC…

防御保护第四次作业

防火墙的智能选路 就近选路 --- 我们希望在访问不同运营商的服务器是&#xff0c;通过对应运营商的链路。这样可以高 通信效率&#xff0c;避免绕路。 策略路由 -- PBR 传统的路由&#xff0c;仅基于数据包中的目标IP地址查找路由表。仅关心其目标&#xff0c;所以&#…

排序【数据结构】

文章目录 一、 稳定性二、排序1. 插入排序(1) 直接插入排序(2) 希尔排序 2. 选择排序(1) 直接选择排序(2) 堆排序 3. 交换排序(1) 冒泡排序(2) 快速排序① 普通版快排② 关于优化快排③ 快速排序的非递归方式 4. 归并排序5. 计数排序 三、 总结 一、 稳定性 在计算机科学中&am…

CHS_03.2.3.2_2+进程互斥的硬件实现方法

CHS_03.2.3.2_2进程互斥的硬件实现方法 知识总览中断屏蔽方法TestAndSet指令Swap指令 知识回顾 进程互斥的四种软件实现方法 知识总览 这个小节我们会介绍另外的三种进程互斥的硬件实现方法 那么 这个小节的学习过程当中 大家需要注意理解各个方法的原理 并且要稍微的了解各个…

【Uni-App】Vue3如何使用pinia状态管理库与持久化

安装插件 pinia-plugin-unistorage 引入 // main.js import { createSSRApp } from "vue"; import * as Pinia from "pinia"; import { createUnistorage } from "pinia-plugin-unistorage";export function createApp() {const app create…

SpringBoot不同的@Mapping使用

文章目录 一、介绍二、使用 一、介绍 一般Mapping类注解在Spring框架中用于将HTTP请求映射到对应的处理器方法。它们各自对应于不同类型的HTTP方法&#xff0c;主要用于RESTful Web服务中。以下是每个注解的作用&#xff1a; GetMapping: 用于映射HTTP GET请求到处理器方法。通…

Life is Strange 奇异人生汉化指南

奇异人生汉化指南 引言&#xff1a;在搜索引擎上看了许多的攻略&#xff0c;都无法得到指向性明确的安装步骤&#xff0c;其中最令人不解的分别为汉化包与汉化包的安装地址&#xff0c;以下会以汉化包获取与汉化包安装地址两个维度来确保汉化的正确&#xff0c;以及在最终附上…

爬虫学习笔记-get请求获取豆瓣电影排名多页数据★★★★★

1. 导入爬虫需要使用的包 import urllib.request import urllib.parse 2.创建请求函数 def create_request(page): # 定义不变的url部分 base_url https://movie.douban.com/j/chart/top_list?type5&interval_id100%3A90&action& # 根据规律定义data拼接url …