【Linux】Linux下多线程

news2024/11/18 11:37:16

需要云服务器等云产品来学习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:要被等待的线程id
	retval:指向一个存放线程返回值的指针,(线程返回值的类型是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传入线程运行时所需要的全部内容(构造成一个对象传入)Context
        Context *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_;    // 线程id
    func_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.coloradmin.cn/o/1419397.html

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

相关文章

练习12.6_横向射击_Python编程:从入门到实践(第3版)

编写一个游戏&#xff0c;将一艘飞船放在屏幕左侧&#xff0c;并允许玩家上下移动飞船。在玩家按空格键时&#xff0c; 让飞船发射一颗在屏幕中向右飞行的子弹&#xff0c;并在子弹从屏幕中消失后将其删除。 ship_shooting.py import pygame import sys from leftship impor…

​ArcGIS Pro 如何批量删除字段

在某些时候&#xff0c;我们得到的图层属性表内可能会有很多不需要的字段&#xff0c;如果挨个去删除会十分的麻烦&#xff0c;对于这种情况&#xff0c;我们可以使用工具箱内的字段删除工具批量删除&#xff0c;这里为大家介绍一下使用方法&#xff0c;希望能对你有所帮助。 …

[C++历练之路]C++中的继承小学问

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; &#x1f354;前言&#xff1a; C中&#xff0c;继承是一种面向对象编程的重要概念&#xff0c;它允许一个类&#xff08;子类/派生类&#xff09;从另一个类&#xff08;父类/基类&#xff09;继承属性和方法。继承是…

C语言系列-整数在内存中的存储大小端字节序

&#x1f308;个人主页: 会编程的果子君 ​&#x1f4ab;个人格言:“成为自己未来的主人~” 目录 整数在内存中的存储 大小端字节序和字节序判断 什么是大小端 为什么会有大小端 练习 整数在内存中的存储 在讲解操作符的时候&#xff0c;我们就讲过了下面的内容 整数的2…

HashMap基本使用

特点&#xff1a; ①HashMap是Map里面的一个实现类。②没有额外需要学习的特有方法&#xff0c;直接使用Map里面的方法就可以了。③特点都是由键决定的&#xff1a;无序、不重复、无索引④HashMap跟HashSet底层原理是一模一样的&#xff0c;从名字可以看出来&#xff0c;都是哈…

MySQL原理(一)架构组成(2)逻辑模块组成

总的来说&#xff0c;MySQL可以看成是二层架构&#xff0c;第一层我们通常叫做SQL Layer&#xff0c;在MySQL数据库系统处理底层数据之前的所有工作都是在这一层完成的&#xff0c;包括权限判断&#xff0c;sql解析&#xff0c;执行计划优化&#xff0c;query cache的处理等等&…

麒麟系统—— openKylin 安装 Nacos

麒麟系统—— openKylin 安装 Nacos 一、准备工作1. 确保麒麟系统 openKylin 已经安装完毕。2. 确保 java 已经安装完毕3. 确保 Maven 已经安装完毕 二、下载 nacos三、解压与运行解压 关于 nacos 配置 本文将分享如何在麒麟系统 openKylin 上安装 Nacos。 一、准备工作 1. …

深度学习之卷积神经网络

卷积神经网络简称为CNN 首先我们来回顾一下&#xff0c;我们之前学到的全连接的神经网络&#xff1a; 上面我们通过线性层串行连接起来的神经网络&#xff0c;我们叫做全链接的网络&#xff0c;在线性层里面&#xff0c;我们的输入值和任意的输出值之间都存在权重&#xff0c;…

《HTML 简易速速上手小册》第10章:HTML 的维护与优化(2024 最新版)

文章目录 10.1 网页性能优化10.1.1 基础知识10.1.2 案例 1&#xff1a;优化网页图像10.1.3 案例 2&#xff1a;使用延迟加载优化性能10.1.4 案例 3&#xff1a;优化 CSS 和 JavaScript 的加载 10.2 SEO 最佳实践10.2.1 基础知识10.2.2 案例 1&#xff1a;创建一个 SEO 友好的博…

伊恩·斯图尔特《改变世界的17个方程》毕达哥拉斯定理笔记

它告诉我们什么&#xff1f; 直角三角形的三个边之间有什么关系。 为什么重要&#xff1f; 它提供了几何和代数之间的重要联系&#xff0c;使我们能够根据坐标计算距离。它也催生出了三角学。 它带来了什么&#xff1f; 测绘、导航&#xff0c;以及较近代出现的狭义和广义相对论…

深入了解Matplotlib中的子图创建方法

深入了解Matplotlib中的子图创建方法 一 add_axes( **kwargs):1.1 函数介绍1.2 示例一 创建第一张子图1.2 示例二 polar参数的运用1.3 示例三 创建多张子图 二 add_subplot(*args, **kwargs):2.1 函数介绍2.2 示例一 三 两种方法的区别3.1 参数形式3.2 布局灵活性3.3 适用场景3…

基于YOLOv8的摄像头吸烟行为检测系统(Python源码+Pyqt6界面+数据集)

&#x1f4a1;&#x1f4a1;&#x1f4a1;本文主要内容:详细介绍了摄像头下吸烟行为检测系统&#xff0c;在介绍算法原理的同时&#xff0c;给出Pytorch的源码、训练数据集以及PyQt6的UI界面。在界面中可以选择各种图片、视频进行检测识别&#xff0c;可进行置信度、Iou阈值设定…

【linux】磁盘空间不足-常用排查和处理命令

【linux】磁盘空间不足-常用排查和处理命令 1.通查一下 df -h #查看服务器磁盘空间情况 du -hs * 2>/dev/null #列出各目录所占空间大小 或 du -h -d 1 2>/dev/null #列出各目录所占空间大小 1.1情况一 df 磁盘空间和du 目录空间占用相等&#xff0c…

离线安装nginx_银河麒麟系统_nginx报错_503_500 Internal Server Error----nginx工作笔记007

如果报这个错误,意思就是,对于nginx.conf文件中指定的,文件夹没有权限 那么这个是去给对应的文件夹赋权限: chmod 777 /opt/module/test_web 就可以了,然后再去访问就不会报错了,还有 503的错误都可以这样解决 然后关于离线安装nginx,尝试了一下如果把之前安装过的nginx,直接…

app逆向-frida定位签名校验

文章目录 一、前言二、如何实现签名校验三、案例&#xff1a;定位签名校验 一、前言 当我们说应用签名校验时&#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…

Centos Cron设置定时任务

这本是很简单的问题&#xff0c;但是我服务器重装系统两次&#xff0c;遇到的问题都不一样&#xff0c;所以记录一下 1.首先要确保服务器上有 cron 服务 sudo systemctl status crond2.设置时区 sudo timedatectl set-timezone Asia/Shanghai3.重启crond 服务使crond服务的时…

指针的深入理解(一)

这一节主要复习数组指针&#xff0c;int (* )[ ] 就是数组指针类型的标志。 因为有&#xff08;&#xff09;将*括起来&#xff0c;所以&#xff08;*&#xff09;表示一个指针。[ ] 表示数组&#xff0c;所以&#xff08;*&#xff09;[ ]就表示一个指向数组的指针&#xff…

【C++】I/O多路转接详解(一)

目录 1. 背景引入1.1 IO的过程1.2 五种IO模型1.2.1 阻塞IO1.2.2 非阻塞IO1.2.3 信号驱动IO1.2.4 IO多路转接1.2.5 异步IO 1.3 同步通信 与 异步通信1.4 阻塞 与 非阻塞1.4.1 阻塞与非阻塞区别1.4.2 设置非阻塞IO 2. select2.1 接口使用2.2 select执行过程2.3 select代码实践 3.…

C++ 数论相关题目:卡特兰数应用、快速幂求组合数。满足条件的01序列

给定 n 个 0 和 n 个 1 &#xff0c;它们将按照某种顺序排成长度为 2n 的序列&#xff0c;求它们能排列成的所有序列中&#xff0c;能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。 输出的答案对 1097 取模。 输入格式 共一行&#xff0c;包含整数 n 。 …