当前位置: 首页 > news >正文

【Linux】多线程(一万六千字)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

文章目录

前言

线程的概念

线程的理解(Linux系统为例)

在Linux系统里如何保证让正文部分的代码可以并发的去跑呢?

为什么要有多进程呢?

为什么要这么设计Linux"线程"?

线程的优点

线程的缺点

线程异常

线程用途

Linux进程 VS 线程

进程和线程

进程的多个线程共享

进程和线程的关系

关于调度的问题

再次谈谈进程地址空间

多个执行流是如何进行代码划分的?如何理解?

OS如何管理内存呢?

线程的控制

POSIX线程库

创建线程

PID和LWP

Linux中有没有真线程呢?

线程ID及进程地址空间布局

线程终止

线程等待

分离线程

面试题

多线程创建

总结



前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

线程的概念

  • 线程是进程内部的一个执行分支,线程在进程的地址空间内运行。
  • 线程是CPU调度的基本单位,CPU在调度的时候,会有很多进程和线程混在一起,但是CPU不管这些,在调度的时候,都是让task_struct进行排队的,CPU只调度task_struct,所以说线程是CPU调度的基本单位是对的。

线程的理解(Linux系统为例)

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

  • 正文:代码段(区),我们的代码在进程中,全部都是串行调用的。
  • 就一个进程,正文部分有很多对应的函数,但我们在执行的时候,所有的函数调用都是串行调用的。
  • 比如:main()函数中有a、b、c、d四个函数,我们单进程执行main()函数时,所有的函数都是串行跑的;那么今天我们想办法将代码拆成两部分,a、b函数一部分,c、d函数一部分,让一个执行流执行a、b,让另一个执行流执行c、d函数,如果a、b和c、d函数没有明显的前后关系的话,分成两个执行流让它能跑,那么此时我们的函数调用过程就是并行跑了。

无论是多进程,还是多线程,它的核心思想:把串行的东西变成并行的东西。

在Linux系统里如何保证让正文部分的代码可以并发的去跑呢?

  • 以前:再创建进程PCB、进程地址空间、页表,再从磁盘中向物理内存中加载新的程序,经过新创建进程的页表与物理内存建立映射关系,此时就有了独立的代码和数据,独立的内核数据结构,所以这两个进程是独立的。但是我们发现按照之前的做法,进程创建的成本(时间和空间)是非常高的。
  • 用户想要的是多执行流,所以Linux创建了一个线程,假设正文部分有很多的代码,想办法将代码分为若干份区域,比如三份区域,进程地址空间中的其它区域可以都看到,再创建一个执行流的时候,不用创建地址空间和页表,只需要在地址空间内创建两个新的task_struct,让两个新的task_struct指向同一块进程地址空间,那么它们就能看到同一份地址空间的资源,让A进程用第一个区域,让B进程用第二个区域,让C进程用第三个区域,那么CPU调度的时候,拿着三个task_struct,把当前进程的串行执行的三份代码,变成了并发式执行这三份代码了,所以我们把这种在地址空间内创建的"进程",把它叫做线程。

进程地址空间上布满了虚拟地址,进程地址空间以及上面的虚拟地址的本质是一种资源。

我们之前说的,代码可以并行或并发的去跑,比如:父子进程的代码是共享的,数据写实拷贝各自一份,所以可以让父子执行不同的代码块,这样就可以将代码块进行两个各自调度运行了。

为什么要有多进程呢?

目标不是为了多进程,是为了多执行流并发执行,为了让多个进程之间可以并发的去跑相同或不同的代码。

为什么要这么设计Linux"线程"?

线程跟进程一样,也是要被调度的。
线程在一个进程内部,就意味着一个进程内部可能会存在很多个线程。
如果我们要设计线程,OS也要对线程进行管理!先描述,再组织。描述线程:线程控制块(struct TCB),要保证线程被OS管理,比如用链表将线程管理,还要保证进程PCB和这些线程进行关联,PCB中的对应的指针指向对应的线程,但是这样是非常复杂的。

  • 管理线程的策略和进程是非常像的,OS要求我们对应的线程在进程内运行,是进程内的执行分支,只要符合这个特点,就都是线程,并不一定必须上面的实现。管理进程已经设计数据结构,设计调度算法,还写了创建、等待、终止等各种接口,那么可以把进程的数据结构和调度算法等代码复用起来。
  • Linux的设计者认为,进程和线程都是执行流,具有极度的相似性,没有必要单独设计数据结构和调度算法,直接复用代码。
  • 使用进程来模拟线程。
  • Windows是单独的设计了线程模块。Linux用的是复用进程的代码来设计的。

线程的优点

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

线程的缺点

性能损失

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

健壮性降低

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

缺乏访问控制

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

编程难度提高

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

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出

线程用途

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

Linux进程 VS 线程

进程和线程

  • 以前的进程:一个内部只有一个线程的进程。
  • 今天的进程:一个内部至少有一个线程的进程。我们以前的讲的进程是今天讲的进程的一种特殊情况。

什么是进程呢?

  • 内核的数据结构+进程的代码和数据(也就是一个或者多个执行流、进程地址空间、页表和进程的代码和数据)
  • 线程(task_struct)叫做进程内部的一个执行分支。
  • 线程是调度的基本单位。
  • 进程的内核角度:承担分配系统资源的基本实体。
  • 不要站在调度角度理解进程,而应该站在资源角度理解进程。

线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

讲一个故事:

  • 我们的社会就是一个大的系统,在社会中承担分配社会资源(汽车、彩电等)的基本实体是家庭,家庭中的每一个人都是一个执行流,各自都做着不同的事情,但每一个人都会互相协作起来,完成一个公共的事情,把日子过好。家庭中的每一个人就是线程,家庭就是一个进程。

进程的多个线程共享

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

  • 如果定义一个函数,在各线程中都可以调用;
  • 如果定义一个全局变量,在各线程中都可以访问到;

除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系

关于调度的问题

CPU在选择执行流去调度的时候,用不用区分一个进程内部唯一的执行流呢?还是一个进程内部某一个执行流呢?
不需要管。因为线程也有PCB、进程地址空间、页表、进程代码和数据,与进程一致,都是执行流,不需要区分。

  • CPU调度的执行流:线程 <= 执行流 <= 进程
  • Linux是用进程模拟的线程,所以Linux系统里严格意义上讲,不存在物理上的真正的线程,因为没有单独为线程创建struct TCB。
  • Linux中,所有的调度执行流都叫做:轻量级进程。

再次谈谈进程地址空间

多个执行流是如何进行代码划分的?如何理解?

  • 如果进程执行的代码也是一个操作系统,这就相当于使用了进程的壳子,完成了一种内核级虚拟机的技术。
  • OS要不要管理内存呢?大部分OS中的内存在系统角度是以4KB为单位的内存块。
  • 一个可执行程序加载是以平坦模式把整个代码进行编址的,对应的可执行程序也在内部按地址划分成4KB的数据块。
  • 可执行程序里的二进制代码只要写到文件系统里,天然就是4KB的。
  • 从此磁盘和物理内存进行交互时,就以数据块为单位,这就叫做4KB数据块。
  • 内存中的4KB数据块叫做空间;磁盘中文件的4KB数据块叫做内容;所谓的加载,就是将内容放入空间当中,在OS的术语里,一般我们把4KB的空间或内容叫做页框或者页帧。

OS如何管理内存呢?

先描述,再组织!用struct page结构体来描述内存中的4KB数据块,假设有万4GB,4GB内存中有100多个4KB的数据块,用struct page mem[100多万]数组来组织,天然的每一个4KB就有了它的编号(物理地址),编号就是下标,对内存进行管理,就是对该数组的增删查改。未来加载程序时,有多少个4KB的数据块要加载,我们就在内存当中申请多少个数据块,将程序数据块中的内容加载到数组下标中的数据块空间中。

  • OS进行内存管理的基本单位是4KB。
  • 所以以前讲过的父子进程代码共享,数据各自私有一份,内存块中保存的代码,它里面配置的引用计数就是2(父子进程都指向它),所以子进程退出了,不影响父进程,引用计数--;写实拷贝是以4KB为单位进行的,不是只以变量为单位进行的,像new、malloc申请对象的时候,在OS也是以4KB为单位申请空间的。
  • 可执行程序没有被加载之前,就已经有虚拟地址了,加载到物理内存之后,程序内部用虚拟地址,定位我们的程序用物理地址。

以32位平台下为例:
将虚拟地址转换成物理地址:

  1. 虚拟地址(32byte)不是铁板一块,虚拟地址被OS看成10、10、12三个子区域,
  2. OS在进程创建、加载时,根本就不需要搞一个大页表,而只需要,从左往右数的前10个比特位(第一个子区域),这10个比特位从全0到全1的取值范围为:[0,1023]/[0~2^10-1];第二个子区域的范围:[0,1023];
  • 在刚开始创建进程的时候,必须给进程创建页表,这句话没错,但刚开始创建的不是完整的页表,我们只需要创建第一个子区域的页表,页表中有1024个项,查页表时,需要先拿虚拟地址的前10个比特位检索这张表,这张表叫做页目录。
  • 第二个子区域也要创建一张或者多张表,这些表才是页表,我们把页表和页目录里面的条目叫做页表项,页目录里面保存的是二级页表的地址,在查页表时,先拿虚拟地址的前10个比特位做第一张表的索引,再拿虚拟地址的中间10个byte来查页表,OS当中最多会存在1024张页表(不可能的);页表中存放的是物理内存中每一个4KB数据块的起始地址,假设访问一个页表中存放4KB数据块的起始地址0x1234,访问的并不是4KB的数据块,而是访问的是4KB里面的某一个区域或字节,因为线性地址,地址空间的基本单位是1字节的,可以0x1234 + 虚拟地址后的12位(第三个子区域)对应的数据 == 访问到4KB数据块的全部内容。
  • 为什么是12位呢?因为2^12就是4KB。虚拟地址的后12位,我们称之为页内偏移。所以我们查页表只是用虚拟地址的前20位btye。页表里面保存的是页框的物理地址。
  • 页目录占4KB空间,页表最多占4MB空间,所以整个页表内容,我们用4MB就能表示完了。
  • 页表中也可以加一些标志位,表示对应的数据块是内核用的,还是用户用的,还有权限等。

结论:给不同的线程分配不同的区域,本质就是给让不同的线程,各自看到全部页表的子集。就是让不同的线程看到不同的页表。

线程的控制

POSIX线程库

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

创建线程

功能:创建一个新的线程
原型
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);
参数参数1:输出型参数,创建成功会带出新线程id;参数2: 设置线程的属性,attr为NULL表示使用默认属性参数3:返回值为void* ,参数为void* 的函数指针,让新线程来执行这个函数方法参数4:传递给线程函数的参数,参数会传递到参数3中去
返回值:成功返回0;失败返回错误码

内部创建线程之后,将来会有两个执行流,一个是主线程,一个是新创建的线程,新创建的线程会回调式的调用参数3(函数指针)。

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误, 建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include <iostream>
#include <pthread.h>  // 在Liinux中使用线程,要包含头文件
#include <unistd.h>
#include <sys/types.h>// 新线程
void* newthreadrun(void* args)
{while (true){std::cout << "I am new thread, pid: " << getpid() << std::endl;sleep(1);}
}int main()
{pthread_t tid;// 线程id// 创建新线程pthread_create(&tid, nullptr, newthreadrun, nullptr);while (true){std::cout << "I am main thread, pid: " << getpid() << std::endl;sleep(1);}
}

PID和LWP

  • ps -aL中的L:能查看真实存在的多线程(轻量级进程)
  • OS在进行调度的时候,用轻量级进程(LWP)的id来进行调度。
  • 单进程和多进程在调度的时候,也在看LWP,因为当每一个进程内部都只有一个执行流时,LWP == PID,此时调用那个都是一样的。
  • LWP和PID说明PCB里面每一个都包含PID,PID表示PCB属于哪一个进程;LWP表明PCB是进程中的那个执行流。
  • getpid()获得进程的pid,而不是获取的是LWP的id,OS没有直接提供获取LWP的id的系统调用。
  • 函数编译完成,是若干行代码块,每一行代码都有地址(虚拟地址/磁盘上-逻辑地址),函数名是该代码块的入口地址。所有的函数,都要按照地址空间统一编址。
  • ps -aL:查看的轻量级进程,所看到的LWP是线程的id,与pthread_create()函数中参数1所得到的新线程的id,两者的表现形式是不同的,因为LWP是在内核当中来标识一个执行流的唯一性的,所以只在OS内使用,但是创建线程pthread_create(),用的线程是属于线程库,所以pthread_create()函数的参数1得到的id是线程库来维护的。这两个id是一对一的,一个是在用户层的,一个是在内核层的。

Linux中有没有真线程呢?

  1. 没有。Linux中只有轻量级进程。
  2. 为了保证自己的纯洁性和简洁性,所以Linux系统,不会有线程相关的系统调用,只有轻量级进程的系统调用。
  3. 为了让用户选择Linux系统,为了让用户能正常的使用对应的线程功能,Linux设计者在用户和Linux系统之间设计了一个中间的软件层,软件层叫做pthread库(原生线程库),任何的Linux内核里面,你在安装的时候,pthread库必须在Linux系统里自带,在系统里默认就装好了,pthread库的作用是将轻量级进程的系统调用进行封装,转成线程相关的接口语义提供给用户,底层其实还是轻量级进程。

用户知道"轻量级进程"这个概念吗?

没有。用户只认进程和线程。其实轻量级进程就是线程。

pthread库不属于OS内核,只要是库就是在用户级实现的,所以Linux的线程也别叫做用户级线程。所以编写多线程时,都必须要链接上这个pthread库:-lpthread

testthread:testThread.ccg++ - o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm - f testthread

线程ID及进程地址空间布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
man pthread_self // 那个线程调用pthread_self()函数,就获取那个线程的id
pthread_t pthread_self(void);

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质 就是一个进程地址空间上的一个地址。

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

功能:线程终止
原型
    void pthread_exit(void* value_ptr);// 那个线程调用该函数,就终止那个线程
参数
    value_ptr: value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函 数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

功能:取消一个执行中的线程
原型
    int pthread_cancel(pthread_t thread);
参数
    thread : 线程ID
返回值:成功返回0;失败返回错误码

线程等待

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

man pthread_join  
int pthread_join(pthread_t thread, void **value_ptr); 等待一个已经结束的线程

  • 参数1:等待指定的一个线程,如果该线程没有退出,会阻塞式等待,若该线程退出了,则返回等待的结果;
  • 参数2:输出型参数,拿到的是新线程对应的返回值

返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED,就是-1。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

分离线程

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

把一个线程设置成分离:
man pthread_detach  
int pthread_detach(pthread_t thread);
参数:要分离哪一个线程的id

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

 pthread_detach(pthread_self()); 

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

// 同一个进程内的线程,大部分资源都是共享的. 地址空间是共享的!
// 比如:初始化和未初始化区域、还有正文部分没有被线程分走的其它代码也是这个进程所有的线程共享的。
int g_val = 100;// 将新的线程id转换成16进制的形式
std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}// 线程退出// 1. 代码跑完,结果对// 2. 代码跑完,结果不对// 3. 出异常了 --- 重点 --- 多线程中,任何一个线程出现异常(div 0, 野指针), 都会导致整个进程退出! // ---- 多线程代码往往健壮性不好void *threadrun(void *args){// 将函数的参数传递过来,字符串的地址来用于初始化新线程的名字std::string threadname = (char*)args;int cnt = 5;while (cnt){printf("new thread, g_val: %d, &g_val: %p\n", g_val, &g_val);// std::cout << threadname << " is running: " << cnt << ", pid: " << getpid()//     << " mythread id: " << ToHex(pthread_self())//     << "g_val: "<< g_val << " &g_val: " << &g_val << std::endl;g_val++;sleep(1);// int *p = nullptr;// *p = 100; // 故意一个野指针cnt--;}// 1. 线程函数结束 法1:(return)// 2. 法2:pthread_exit()// pthread_exit((void*)123);// 终止新线程// exit(10); // 不能用exit终止线程,因为它是终止进程的.return (void*)123; // warning}// 主线程退出 == 进程退出 == 所有线程都要退出(资源都被释放)// 1. 往往我们需要main thread最后结束// 2. 线程也要被"wait", 要不然会产生类似进程哪里的内存泄漏的问题(线程是需要被等待的)int main(){// 1. idpthread_t tid;// pthread_t就是一个无符号的长整型pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");// 参数1:输出型参数,得到的是新线程的id   // 法3: // 在主线程中,你保证新线程已经启动// sleep(2);// pthread_cancel(tid);// 取消tid线程,那么pthread_join()函数拿到的就是线程的退出码-1,-1就是宏,-1表示这个线程是被取消的// 2. 新和主两个线程,谁先运行呢?不确定,由调度器决定int cnt = 10;while (true){std::cout << "main thread is running: " << cnt << ", pid: "<< getpid() << " new thread id: " << ToHex(tid) << " "<< " main thread id: " << ToHex(pthread_self())<< "g_val: "<< g_val << " &g_val: " << &g_val << std::endl;printf("main thread, g_val: %d, &g_val: %p\n", g_val, &g_val);sleep(1);cnt--;}// 如果主线程比新线程提前退出了呢?void* ret = nullptr;// void:是不能定义变量的;void*:能定义变量,指针变量是已经开辟了空间的// PTHREAD_CANCELED; // (void*)-1// 我们怎么没有像进程一样获取线程退出的退出信号呢?只有你手动写的退出码// 所等的线程一旦产生信号了,线程所在的进程就被干掉了,所以pthread_join没有机会获得信号。// 所以pthread_join()函数不考虑线程异常情况!int n = pthread_join(tid, &ret); std::cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;return 0;}

新线程所产生的异常由父进程去考虑。

  std::string ToHex(pthread_t tid){char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;}__thread uint64_t starttime = 100;// __thread int tid = 0;// 全局变量g_val属于已初始化区域,是所有线程共享的资源// __thread:让这个进程中所有的线程都私有一份g_val全局变量// __thread:编译器在编译时将g_val变量拆分出来,放到了每个线程的局部存储空间内int g_val = 100;// 主线程一直在等待新线程,在等待期间,不会创造价值,所以有类似于非阻塞等待:// 线程是可以分离的: 默认线程是joinable(需要被等待)的。// 如果我们main thread不关心新线程的执行信息,我们可以将新线程设置为分离状态:// 你是如何理解线程分离的呢?底层依旧属于同一个进程!只是不需要等待了// 一般都希望mainthread 是最后一个退出的,无论是否是join、detachvoid *threadrun1(void *args){starttime = time(nullptr);// pthread_detach(pthread_self());// 该线程自己分离自己,则主线程不会再等待新线程std::string name = static_cast<const char *>(args);while(true){sleep(1);printf("%s, g_val: %lu, &g_val: %p\n", name.c_str(), starttime, &starttime);}return nullptr;}void *threadrun2(void *args){sleep(5);starttime = time(nullptr);// pthread_detach(pthread_self());std::string name = static_cast<const char *>(args);while(true){printf("%s, g_val: %lu, &g_val: %p\n", name.c_str(), starttime, &starttime);sleep(1);}return nullptr;}int main(){pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, threadrun1, (void *)"thread 1");pthread_create(&tid2, nullptr, threadrun2, (void *)"thread 2");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);// pthread_detach(tid);// 可以由主线程来进行使新线程进行分离// std::cout << "new tid: " << tid << ", hex tid: " << ToHex(tid) << std::endl;// std::cout << "main tid: " << pthread_self() << ", hex tid: " << ToHex(pthread_self()) << std::endl;// int cnt = 5;// while (true)// {//     if (!(cnt--))//         break;//     std::cout << "I am a main thread ..." << getpid() << std::endl;//     sleep(1);// }// std::cout << "main thread wait block" << std::endl;// 主线程要等待新线程,否则会出现类似于僵尸进程的问题// 若是新线程是分离的状态,等待的话,会出错返回int n = pthread_join(tid, nullptr);std::cout << "main thread wait return: " << n << ": " << strerror(n) << std::endl;return 0;}

面试题

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 当一个进程被CPU调度时,CPU内部一定存在非常多的寄存器,寄存器当中保存的都是当前进程运行的上下文数据,比如:把CPU内部寄存器的值保存到PCB当中,下次再调用时,再恢复过来;
  • 寄存器CR3保存了页表的起始地址,CPU要切换一个进程的话,我们只需要把PCB、进程地址空间、页表切换就行了;
  • 今天再加了一个线程,线程也是调度的实体,当切换线程时,CPU内部的寄存器中也会有各种的数据,那么线程切换也要进行上下文的保护,将数据保存到线程PCB当中,需要再恢复出来,线程切换时,进程地址空间和页表不用换;
  • 进程切换的时候,进程地址空间和页表要切换;
  • PCB指向进程地址空间,只要PCB切换了,对应的进程地址空间也就切换了,切换一个页表就是切换了CPU中存储页表地址的寄存器CR3;
  • 两者比较一下:线程切换,不切换页表,进程切换,切换一下页表就可以了;
  • 线程切换需要保存的上下文数据,只比进程少一点。(页表不需要切换 == 寄存器CR3不需要改变)

在CPU内部,每一次我们读取当前进程的代码和数据时,CPU上硬件上有一个cache,cache是一个集成在CPU内部的,一段比寄存器容量大的多的一段缓存区,当CPU将虚拟地址转物理地址,进行寻址的时候,找到物理内存中的代码,只找到了一行代码,但是下一次大概率还需要这一行代码的下一行代码,所以会将这块相关的代码全部搬到CPU内部的cache中缓存起来,从此CPU访问代码数据的时候,不用从内存中读取了,而直接从CPU中较近的cache中读取,从而大大提高CPU寻址的效率。

进程间切换时,假设A进程被切换下去,那么CPU内部cache中的数据就被清空,由新切换上来的B进程来重新填充cache中的代码和数据,这个过程很漫长,因此进程间的切换,成本很高。对于线程切换来说,因为进程地址空间、页表、进程的代码和数据都是共享的,所以CPU中cache的缓存区中的数据不需要被丢弃,所以线程切换的成本要比进程要低。

一组寄存器:

  • 每个线程都是独立的,被单独调度的执行流,每个线程都要有一组自己独立的上下文数据。

线程都有自己的临时变量,在C语言中在函数中的临时变量都是在栈区上保存的,比如:主线程要形成自己的临时变量,新线程也要形成自己的临时变量,函数调用要压栈和出栈,如果两个线程使用的是同一个进程地址空间上的栈区,两个都在访问这个栈区,如果一个栈区被多个线程共享的话,每个线程都要向栈区中压栈入自己的临时数据,那么在栈中压入的临时变量,无法分清是那个线程的,所以库在设计的时候,都必须保证给每个线程都要有自己独立的用户栈。每个线程都有自己独立的栈结构。

哪些属于线程私有的?

  1. 线程的硬件上下文(CPU寄存器的值)(调度)
  2. 线程的独立栈结构(常规运行)

线程共享:

  1. 代码和全局数据;
  2. 进程的文件描述符表

一个线程出问题,导致其它线程也出问题,导致整个进程退出---线程安全问题。

多线程中,公共函数如果被多个线程同时进入---该函数被重入了。

多线程创建

#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h> // 原生线程库的头文件const int threadnum = 5;class Task
{
public:Task(){}void SetData(int x, int y){datax = x;datay = y;}// 执行的任务int Excute(){return datax + datay;}~Task(){}
private:int datax;int datay;
};class ThreadData : public Task
{
public:ThreadData(int x, int y, const std::string& threadname) :_threadname(threadname){_t.SetData(x, y);}std::string threadname(){return _threadname;}int run(){return _t.Excute();}
private:std::string _threadname;Task _t;
};
// 结果
class Result
{
public:Result() {}~Result() {}void SetResult(int result, const std::string& threadname){_result = result;_threadname = threadname;}void Print(){std::cout << _threadname << " : " << _result << std::endl;}
private:int _result;std::string _threadname;
};// 每个线程都会执行这个函数
void* handlerTask(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);std::string name = td->threadname();Result* res = new Result();int result = td->run();res->SetResult(result, name);std::cout << name << "run result : " << result << std::endl;delete td;sleep(2);return res;// 这个函数没有使用全局变量,在函数中定义的threadname变量在自己的独立栈上,所以多个线程并不影响// 虽然该函数重入了,但是函数并不会出问题// // std::string threadname =static_cast<char*>(args);// const char *threadname = static_cast<char *>(args);// while (true)// {//     std::cout << "I am " << threadname << std::endl;//     sleep(2);// }// 虽然对于线程来说堆空间是共享的,但是每个线程都只能拿到自己堆空间的起始地址,其它线程的堆空间看不到// delete []threadname;// return nullptr;
}
// 1. 多线程创建
// 2. 线程传参和返回值,我们可以传递基本信息,也可以传递其他对象(包括你自己定义的!)
// 3. C++11也带了多线程,和我们今天的是什么关系??? 
int main()
{std::vector<pthread_t> threads;// 创建5个线程for (int i = 0; i < threadnum; i++){char threadname[64];// 第二次循环时,第一次循环时的缓冲区中的数据就被释放掉或者被后来的数据覆盖snprintf(threadname, 64, "Thread-%d", i + 1);// 将线程名为参数传递给线程函数// 我们不能让每一个线程的threadname的变量都指向同一块缓冲区,我们要给每一个线程申请一个属于自己的空间ThreadData* td = new ThreadData(10, 20, threadname);pthread_t tid;pthread_create(&tid, nullptr, handlerTask, td);threads.push_back(tid);// 将线程id保存到vector中}std::vector<Result*> result_set;// 结果void* ret = nullptr;// 循环等待线程for (auto& tid : threads){pthread_join(tid, &ret);result_set.push_back((Result*)ret);}for (auto& res : result_set){res->Print();delete res;}
}

新线程处于分离状态,新线程无线循环的跑下去,主线程5秒之后,就退出,会发生什么事情呢?

  • 5秒之后,主线程和新线程都会退出。因为主线程(man thread)退出,代表进程结束,那么进程曾经所申请的进程地址空间、页表、代码和数据也会被释放,虽然新线程是分离的,但是依旧是和主线程共享资源的;所谓分离,仅仅是主线程不需要再等待新线程了,不需要关心新线程的执行结果,但资源依旧是共享的。

新线程处于分离状态,新线程无线循环的跑下去,但是新线程中会出现异常,主线程5秒之后,就退出,会发生什么事情呢? 

  • 异常之后,整个进程都会退出。

总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

相关文章:

【Linux】多线程(一万六千字)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 线程的概念 线程的理解(Linux系统为例) 在Linux系统里如何保证让正文部分的代码可以并发的去跑呢&#xff1f; 为什么要有多进程呢&#xff1f; 为…...

sh脚本笔记2

test条件测试 语法 条件测试语法说明语法1&#xff1a;test <测试表达式>这是利用test命令进行条件测试表达式的方法。test命令和“<测试表达式>”之间至少有一个空格语法2&#xff1a;[ <测试表达式> ]这是通过[]&#xff08;单中括号&#xff09;进行条件…...

js替换对象里面的对象名称

data为数组&#xff0c;val为修改前的名称&#xff0c;name为修改后的名称 JSON.parse(JSON.stringify(data).replace(/val/g, name)) &#xff1b; 1.替换data里面的对象tenantInfoRespVO名称替换成tenantInfoUpdateReqVO 2.替换语句&#xff1a; 代码可复制 let tenantInf…...

鸿蒙开发设备管理:【@ohos.settings (设置数据项名称)】

设置数据项名称 说明&#xff1a; 本模块首批接口从API version 8开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 本模块提供设置数据项的访问功能相关接口的说明及示例。 导入模块 import settings from ohos.settings;settings.getUri…...

STM32之五:TIM定时器(2-通用定时器)

目录 通用定时器&#xff08;TIM2~5&#xff09;框图 1、 输入时钟源选择 2、 时基单元 3 、输入捕获&#xff1a;&#xff08;IC—Input Capture&#xff09; 3.1 输入捕获通道框图&#xff08;TI1为例&#xff09; 3.1.1 滤波器&#xff1a; 3.1.2 边沿检测器&#xf…...

【分布式系统】监控平台Zabbix对接grafana

以前两篇博客为基础 【分布式系统】监控平台Zabbix介绍与部署&#xff08;命令截图版&#xff09;-CSDN博客 【分布式系统】监控平台Zabbix自定义模版配置-CSDN博客 一.安装grafana并启动 添加一台服务器192.168.80.104 初始化操作 systemctl disable --now firewalld set…...

操作系统真象还原:编写硬盘驱动程序

第13章-编写硬盘驱动程序 这是一个网站有所有小节的代码实现&#xff0c;同时也包含了Bochs等文件 13.1 硬盘及分区表 13.1.1 创建从盘及获取安装的磁盘数 要实现文件系统&#xff0c;必须先有个磁盘介质&#xff0c;虽然咱们己经有个虚拟磁盘 hd60M.img&#xff0c;但它只…...

firewalld防火墙(二)

一&#xff1a;firewalld高级配置 1&#xff1a;关于iptables的知识 iptables 是Linux系统中传统的命令行防火墙管理工具&#xff0c;它基于内核的netfilter框架工作&#xff0c;用于配置和管理网络规则集&#xff0c;比如过滤&#xff08;允许/拒绝&#xff09;进出的数据包…...

Android-悬浮窗口

在Android系统中&#xff0c;如果应用需要弹出一个悬浮窗口&#xff0c;就需要申请一项特殊权限 <uses-permission android:name"android.permission.SYSTEM_ALERT_WINDOW"/>在Android O之前的系统中申请了该权限后&#xff0c;再给对应的window设置 WindowM…...

打破僵局:Foxit Reader无法打开的终极解决方案

打破僵局&#xff1a;Foxit Reader无法打开的终极解决方案 在数字化阅读时代&#xff0c;Foxit Reader作为一款广受欢迎的PDF阅读器&#xff0c;其打不开的问题无疑会给用户带来诸多不便。本文将为您提供全面的解决方案&#xff0c;从基础检查到高级技巧&#xff0c;确保您能够…...

[调试] JTAG下运行正常,从QSPI或者SD卡启动则无响应,如何查找问题

[调试] JTAG下运行正常&#xff0c;从QSPI或者SD卡启动则无响应&#xff0c;如何查找问题 一、问题现象二、用自定义fsbl替代系统默认的fsbl1. 新建fsbl_new2. 如果提示缺少xilffs库3. 使能调试信息输出 三. 启动成功和失败情况下的典型输出1. JTAG启动模式: 正常加载2. QSPI启…...

Linux内核 -- 多线程之wait_event用法

Linux Kernel 中 wait_event 的高级用法及注意事项 在Linux内核编程中&#xff0c;wait_event 系列函数是用于实现进程等待和事件通知机制的重要工具。本文将详细介绍 wait_event 的高级用法以及注意事项。 1. 基本用法 wait_event 系列宏主要包括以下几种形式&#xff1a; …...

双指针系列第 8 篇:盛水最多的容器。几句话讲明白!

Leetcode 题目链接 思路 取首尾双指针和水量如下所示&#xff0c;设高度函数为 h ( i ) h(i) h(i)&#xff0c;在下图中 h ( l ) < h ( r ) h(l) < h(r) h(l)<h(r)。 观察以 l l l 为左边界所能构成的其他水量&#xff0c;与矮的右边界搭配结果如下。 与高的…...

c++高阶-1-模板

文章目录 模板一、模板基本语法二、函数模板1.基本语法2.函数模板注意事项3.普通函数和函数模板区别4.普通函数和函数模板调用规则 三、类模板1.基本语法2.类模板和函数模板的区别3.类模板中成员函数调用时机4.类模板对象做函数参数5.类模板与继承6.成员函数的类外实现 模板 一…...

.net core 的 winform 的 浏览器控件 WebView2

在.NET Core WinForms应用程序中&#xff0c;没有直接的“浏览器控件”&#xff0c;因为WinForms不支持像WebBrowser控件那样的功能。但是&#xff0c;你可以使用WebView2控件&#xff0c;它是一个基于Chromium的浏览器内核&#xff0c;可以在WinForms应用程序中嵌入Web内容。 …...

Django QuerySet对象,all()方法

all()方法 在Django中&#xff0c;all()方法是QuerySet对象的一个方法&#xff0c;用于获取模型的所有实例。 当你调用ModelName.objects.all()时&#xff0c;Django会生成一个SQL查询&#xff0c;从数据库中获取该模型的所有记录&#xff0c;并返回一个QuerySet对象&#xf…...

自动生成网站sitemap

要在 Next.js 和 Contentlayer 项目中实现自动生成 Sitemap 的功能&#xff0c;你可以编写一个脚本&#xff0c;在每次生成文档后自动生成 Sitemap。以下是一个示例脚本&#xff0c;你可以根据自己的需求进行调整。 步骤 1&#xff1a;安装必要的依赖 首先&#xff0c;你需要…...

中国经济昆虫志(55卷)

中国经济昆虫志&#xff0c;共55卷&#xff0c;内容包括概述、形态特征、分类等。各级分类单元均编有检索表&#xff0c;每个种有特征描述、地理分布&#xff0c;有的还记载有生活习性和防治方法。为便于鉴定&#xff0c;绘制有特征图和彩色图。 包括鞘翅目天牛科、半翅目蝽科、…...

linux环境安装elasticsearch缓存数据库和Kibana客户端

linux环境安装elasticsearch缓存数据库&#xff0c;今天我们安装7.17.18版本&#xff0c;并分析遇到的问题。 一、elasticsearch安装运行 1、直接下载 wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.18-linux-x86_64.tar.gz2、解压 tar -…...

OpenSSL的一些使用案例

目录 一、介绍 二、基本使用 1、Shell &#xff08;1&#xff09;文件加解密 &#xff08;2&#xff09;生成密钥文件 2、API &#xff08;1&#xff09;md5sum &#xff08;2&#xff09;AES256加解密 一、介绍 本篇博客重点不是详细描述 OpenSSL 的用法&#xff0c;只…...

常用字符串方法<python>

导言 在python中内置了许多的字符串方法&#xff0c;使用字符串方法可以方便快捷解决很多问题&#xff0c;所以本文将要介绍一些常用的字符串方法。 目录 导言 string.center(width[,fillchar]) string.capitalize() string.count(sub[,start[,end]]) string.join(iterabl…...

线程池666666

1. 作用 线程池内部维护了多个工作线程&#xff0c;每个工作线程都会去任务队列中拿取任务并执行&#xff0c;当执行完一个任务后不是马上销毁&#xff0c;而是继续保留执行其它任务。显然&#xff0c;线程池提高了多线程的复用率&#xff0c;减少了创建和销毁线程的时间。 2…...

Python28-5 k-means算法

k-means 算法介绍 k-means 算法是一种经典的聚类算法&#xff0c;其目的是将数据集分成 ( k ) 个不同的簇&#xff0c;每个簇内的数据点尽可能接近。算法的基本思想是通过反复迭代优化簇中心的位置&#xff0c;使得每个簇内的点与簇中心的距离之和最小。k-means 算法的具体步骤…...

主流国产服务器操作系统技术分析

主流国产服务器操作系统 信创 "信创"&#xff0c;即信息技术应用创新&#xff0c;作为科技自立自强的核心词汇&#xff0c;在我国信息化建设的进程中扮演着至关重要的角色。自2016年起步&#xff0c;2020年开始蓬勃兴起&#xff0c;信创的浪潮正席卷整个信息与通信技…...

【Linux】线程封装与互斥(万字)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 文章目录 前言 C多线程的用法 对原生线程进行一次封装 理解pthread线程 Linux线程互斥 进程线程间的互斥相关背景概念 互斥量mutex 操作共享变量会有问题的售票…...

5分钟教你部署MySQL8.0环境

此方法基于Windows操作系统&#xff01; 一、在MySQL官网单击downloads&#xff08;下载&#xff09;MySQLhttps://www.mysql.com/cn/ 选择在Windows操作系统下载 二、选择合适的版本 推荐下载第二种&#xff0c;安装时离线安装即可 三、安装MySQL8.0 1、找到MySQL下载完成…...

LLM应用:传统NLP任务

LLM出来以后&#xff0c;知乎上就出现了“传统NLP已死”的言论&#xff0c;但是传统NLP真的就被扔进历史的垃圾桶了吗&#xff1f; 其实&#xff0c;尽管LLM具有出色的通用能力&#xff0c;但仍然无法有效应对低资源领域的自然语言处理任务&#xff0c;如小语种翻译。为了更好地…...

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建Kafka大数据运算环境---任务11:基础环境准备

任务描述 任务主要是安装配置基础环境&#xff0c;主要内容包括&#xff1a; 1、安装java Kafka和ZooKeeper都需要安装Java环境&#xff0c;推荐至少Java8及以上版本 2、安装ZooKeeper ZooKeeper是Kafka集群的必要组件 3、安装kafka Kafka版本包括使用的scala语言版本和kafka版…...

Golang中swtich中如何强制执行下一个代码块

switch 语句中的 case 代码块会默认带上 break&#xff0c;但可以使用 fallthrough 来强制执行下一个 case 代码块。 package mainimport ("fmt" )func main() {isSpace : func(char byte) bool {switch char {case : // 空格符会直接 break&#xff0c;返回 false…...

读书笔记-Java并发编程的艺术-第4章(Java并发编程基础)-第2节(启动和终止线程)

文章目录 4.2 启动和终止线程4.2.1 构造线程4.2.2 启动线程4.2.3 理解中断4.2.4 过期的suspend()、resume()和stop()4.2.5 安全地终止线程 4.2 启动和终止线程 在前面章节的示例中通过调用线程的start()方法进行启动&#xff0c;随着run()方法的执行完毕&#xff0c;线程也随之…...

通俗大白话理解Docker

什么是Docker Docker本质上是一种容器化技术&#xff0c;用于将应用程序及其所有依赖打包到一个标准化的单元中。这些单元&#xff08;容器&#xff09;可以在任何运行Docker的机器上运行。每个容器是相互隔离的&#xff0c;具有自己的文件系统、网络和进程空间。 以下是大白话…...

题解:CF1981C(Turtle and an Incomplete Sequence)

题解&#xff1a;CF1981C&#xff08;Turtle and an Incomplete Sequence&#xff09; Part 1&#xff1a;题意理解 地址链接&#xff1a;CF、洛谷。题面翻译&#xff1a;给定一个长度为 n n n 的序列 a a a&#xff0c;其中有一些元素未知&#xff0c;用 − 1 -1 −1 表示…...

Swift 中强大的 Key Paths(键路径)机制趣谈(上)

概览 小伙伴们可能不知道&#xff1a;在 Swift 语言中隐藏着大量看似“其貌不扬”实则却让秃头码农们“高世骇俗”&#xff0c;堪称卧虎藏龙的各种秘技。 其中&#xff0c;有一枚“不起眼”的小家伙称之为键路径&#xff08;Key Paths&#xff09;。如若将其善加利用&#xff…...

(十二)纹理和采样

纹理 在绘制三角形的过程中&#xff0c;将图片贴到三角形上进行显示的过程&#xff0c;就是纹理贴图的过程 uv坐标 如果如果图片尺寸和实际贴图尺寸不一致&#xff0c;就会导致像素不够用了的问题 纹理与采样 纹理对象(Texture)&#xff1a;在GPU端&#xff0c;用来以一…...

QT创建地理信息shp文件编辑器shp_editor

空闲之余创建一个简单的矢量shp文件编辑器&#xff0c;加深对shp文件的理解。 一、启动程序 二、打开shp文件 三、显示shp文件的几何图形 四、双击右边表格中的feature&#xff0c;主窗体显示选中feature的各个节点。 五、鼠标在主窗体中选中feature的节点&#xff0c;按鼠标左…...

解析Kotlin中扩展函数与扩展属性【笔记摘要】

1.扩展函数 1.1 作用域&#xff1a;扩展函数写的位置不同&#xff0c;作用域就也不同 扩展函数可以写成顶层函数&#xff08;Top-level Function&#xff09;&#xff0c;此时它只属于它所在的 package。这样你就能在任何类里使用它&#xff1a; package com.rengwuxianfun …...

【Java学习笔记】java图形界面编程

在前面的章节中&#xff0c;我们开发运行的应用程序都没有图形界面&#xff0c;但是很多应用软件&#xff0c;如Windows下的Office办公软件、扑克牌接龙游戏软件、企业进销存ERP系统等&#xff0c;都有很漂亮的图形界面。素以需要我们开发具有图形界面的软件。 Java图形界面编程…...

STM32入门笔记(03): ADC(SPL库函数版)(2)

A/D转换的常用技术有逐次逼近式、双积分式、并行式和跟踪比较式等。目前用的较多的是前3种。 A/D转换器的主要技术指标 转换时间 分辨率 例如&#xff0c;8位A/D转换器的数字输出量的变化范围为0&#xff5e;255&#xff0c;当输入电压的满刻度为5V时&#xff0c;数字量每变化…...

2024年7月2日 (周二) 叶子游戏新闻

老板键工具来唤去: 它可以为常用程序自定义快捷键&#xff0c;实现一键唤起、一键隐藏的 Windows 工具&#xff0c;并且支持窗口动态绑定快捷键&#xff08;无需设置自动实现&#xff09;。 卸载工具 HiBitUninstaller: Windows上的软件卸载工具 经典名作30周年新篇《恐怖惊魂夜…...

如何使用Spring Boot Profiles进行环境配置管理

如何使用Spring Boot Profiles进行环境配置管理 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将深入探讨如何利用Spring Boot Profiles来管理不同环境…...

Java错题归纳(二)

1、若有如下接口A的定义&#xff0c;下列哪些类下确实现了该接口&#xff1a;C interface A { void method1(int i); void method2(int j); } A class B implements A{ void method1( ) { } void method2( ) { } } B class B implements A { void method1(int i ) { }…...

Grafana面试题精选和参考答案

目录 Grafana是什么以及它的主要应用场景 Grafana支持的数据源 Grafana的体系结构及主要组件 Grafana如何实现数据的可视化和监控 Grafana支持的图表类型 如何在Grafana中创建和编辑仪表盘 Grafana的查询编辑器功能 Grafana支持的认证方式 Grafana的性能调优建议 Gra…...

Node版本管理工具 fnm 安装使用

fnm 是一个基于 Rust 开发的 Node 版本管理工具&#xff0c;它的目标是提供一个快速、简单且可靠的方式来管理 Node.js 的不同版本。同时&#xff0c;它是跨平台的&#xff0c;支持 macOS、Linux、Windows。&#x1f680; Fast and simple Node.js version manager, built in R…...

vector模拟实现【C++】

文章目录 全部的实现代码放在了文章末尾准备工作包含头文件定义命名空间和类类的成员变量 迭代器迭代器获取函数 构造函数默认构造使用n个值构造迭代器区间构造解决迭代器区间构造和用n个值构造的冲突拷贝构造 析构函数swap【交换函数】赋值运算符重载emptysize和capacityopera…...

《每天5分钟用Flask搭建一个管理系统》第11章:测试与部署

第11章&#xff1a;测试与部署 11.1 测试的重要性 测试是确保应用质量和可靠性的关键步骤。它帮助开发者发现和修复错误&#xff0c;验证功能按预期工作。 11.2 Flask测试客户端的使用 Flask提供了一个测试客户端&#xff0c;可以在开发过程中模拟请求并测试应用的响应。 …...

Landsat数据从Collection1更改为Collection2

目录 问题解决 问题 需要注意!您使用的是废弃的陆地卫星数据集。为确保功能持续&#xff0c;请在2024年7月1日前更新。 在使用一些以前的代码时会遇到报错&#xff0c;因为代码里面用的是老的数据集 解决 对于地表反射率SR&#xff0c;需要在name中&#xff0c;将C01换为C02&…...

《每天5分钟用Flask搭建一个管理系统》第12章:安全性

第12章&#xff1a;安全性 12.1 Web应用的安全威胁 Web应用面临的安全威胁包括但不限于跨站脚本攻击&#xff08;XSS&#xff09;、SQL注入、跨站请求伪造&#xff08;CSRF&#xff09;、不安全的直接对象引用&#xff08;IDOR&#xff09;等。 12.2 Flask-Talisman扩展的使…...

Unity之创建与导出PDF

内容将会持续更新&#xff0c;有错误的地方欢迎指正&#xff0c;谢谢! Unity之创建与导出PDF TechX 坚持将创新的科技带给世界&#xff01; 拥有更好的学习体验 —— 不断努力&#xff0c;不断进步&#xff0c;不断探索 TechX —— 心探索、心进取&#xff01; 助力快速…...

【Android面试八股文】优化View层次过深问题,选择哪个布局比较好?

优化深层次View层次结构的问题&#xff0c;选择合适的布局方式是至关重要的。以下是几点建议&#xff1a; 使用ConstraintLayout&#xff1a;ConstraintLayout是Android开发中推荐的布局&#xff0c;能够有效减少嵌套&#xff0c;提高布局性能。相比RelativeLayout&#xff0c;…...

什么是带有 API 网关的代理?

带有 API 网关的代理服务显著提升了用户体验和性能。特别是对于那些使用需要频繁创建和轮换代理的工具的用户来说&#xff0c;使用 API 可以节省大量时间并提高效率。 了解 API API&#xff0c;即应用程序编程接口&#xff0c;是服务提供商和用户之间的连接网关。通过 API 连接…...

IDEA中使用Maven打包及碰到的问题

1. 项目打包 IDEA中&#xff0c;maven打包的方式有两种&#xff0c;分别是 install 和 package &#xff0c;他们的区别如下&#xff1a; install 方式 install 打包时做了两件事&#xff0c;① 将项目打包成 jar 或者 war&#xff0c;打包结果存放在项目的 target 目录下。…...

Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method--论文笔记

论文笔记 资料 1.代码地址 https://github.com/iBelieveCJM/pseudo_label-pytorch 2.论文地址 3.数据集地址 论文摘要的翻译 本文提出了一种简单有效的深度神经网络半监督学习方法。基本上&#xff0c;所提出的网络是以有监督的方式同时使用标记数据和未标记数据来训练的…...

python 身份证工具

介绍&#xff1a; 可以解析身份证&#xff0c;获取身份证的省&#xff0c;市&#xff0c;区信息&#xff0c;年龄&#xff0c;性别&#xff0c;生日信息 import re from datetime import datetime import jsonclass IDCardParserUtil:def __init__(self):pass# 身份证号码前6位…...

Java [ 基础 ] Java面向对象编程 (OOP) ✨

目录 ✨探索Java基础 Java面向对象编程 (OOP) ✨ 引言 1. 类和对象 2. 封装 3. 继承 4. 多态 5. 抽象 结论 ✨探索Java基础 Java面向对象编程 (OOP) ✨ 引言 Java是一门以面向对象编程&#xff08;OOP&#xff09;为基础的编程语言。OOP的核心概念包括类和对象、封装…...

Android的悬浮时钟(一)

在Android&#xff0c;如果要悬浮在其他应用上方显示时钟或者其他界面的话是需要申请权限的。 首先在manifest中我们就要写自己要申请的权限SYSTEM_ALERT_WINDOW <uses-permission android:name"android.permission.SYSTEM_ALERT_WINDOW" /> 不同于请求照片或…...

opencascade AIS_InteractiveContext源码学习7 debug visualization

AIS_InteractiveContext 前言 交互上下文&#xff08;Interactive Context&#xff09;允许您在一个或多个视图器中管理交互对象的图形行为和选择。类方法使这一操作非常透明。需要记住的是&#xff0c;对于已经被交互上下文识别的交互对象&#xff0c;必须使用上下文方法进行…...

来自90年前的首款银箭赛车,梅赛德斯-奔驰W25创造无数传奇

90年前,这款银色单座赛车奠定了“银箭”这个名称,至今仍适用于梅赛德斯-奔驰的赛车。梅赛德斯-奔驰W25于1934年6月3日在纽博格林的艾菲尔赛事中首次亮相,Manfred von Brauchitsch赢得比赛,创下平均时速122.5公里的新纪录。这是成功故事的开始,延续至今天的梅赛德斯-AMG Pe…...

沃尔沃S90:从设计到实践的安全标杆

“网上没赢过,实战没输过”,沃尔沃用每一次安全事故生动诠释了这句话。虽然包括沃尔沃品牌本身和车主在内,平时都比较低调,是“车圈逐渐粉圈化”大环境下的一股清流,但真正遇到交通事故的时候,你才知道,沃尔沃是真能保命啊。这不,最近就有位沃尔沃S90车主在高速上以100…...

深圳建设“超充之城”提速

五分钟的时间可以做什么?也许只是白领喝一杯咖啡的时间,但在深圳,能给新能源汽车续航200公里。在深圳的超充站,“一杯咖啡,满电出发”的标语十分醒目。深圳随处可见的超充标语近日,记者从深圳市发展改革委获悉,截至5月17日,深圳累计建成超充站378座。深圳“超充之城”建…...

python生成词云图

生成词云图的话需要先对数据进行分词处理 , 分词方法点击查看 import pandas as pd from collections import Counter from wordcloud import WordCloud import matplotlib.pyplot as plt# 假设您已经按照之前的步骤处理了数据&#xff0c;并且处理后的数据保存在comments_proc…...

aws glue配置读取本地kafka数据源

创建连接时填写本地私有ip地址&#xff0c;选择网络配置 配置任务选择kafka作为数据源 但是执行任务时日志显示连接失败 文档提到只能用加密通信 如果您希望与 Kafka 数据源建立安全连接&#xff0c;请选择 Require SSL connection (需要 SSL 连接)&#xff0c;并在 Kafka priv…...

中国新能源产业助推全球绿色发展

中国车企长城汽车旗下电动汽车品牌欧拉好猫1月12日在泰国罗勇新能源汽车制造基地正式下线,这是泰国本土生产的第一款量产纯电动汽车。图为工人在生产车间内工作。新华社发【中国经济亮色与世界发展机遇】在绿色发展理念指引下,中国新能源产业通过多年自主技术创新、供应链整合…...