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

Linux线程概念与线程操作

Linux线程概念与线程操作

线程概念

前面提到进程=程序代码和数据+进程结构体,在线程部分就需要进一步更新之前的认识

进程实际上承担分配系统资源的基本实体,而线程是进程中的一个执行分支,是操作系统调度的基本单位

此处需要注意,操作系统并不是直接调用进程,而是调用线程,所以进程并不是操作系统调度的基本单位

Linux线程的设计

要想更清楚地理解进程就必须深入到一个具体的操作系统中学习,在Linux中,实际上并没有所谓的线程,因为如果存在线程,那么操作系统必定要对线程进行管理,对应的线程也就需要属于自己的结构和调度方案,但是因为线程本质是进程中的一个执行分支(即执行某一段代码,并且这段代码是通过特殊的方式执行的),其中的相关内容与进程非常类似,所以可以不需要额外单独创建一个结构来表示线程,只需要复用进程的PCB结构即可。这样一来,线程所能看到的就是一个进程所有的内容,包括进程的虚拟地址空间、打开的各种文件、各种数据等,这种线程在Linux下统一称为轻量级进程(LWP),之所以轻量,就是因为轻量级进程不需要携带各种资源,只需要执行对应的代码得到对应的结果即可

虽然Linux下只有轻量级进程的概念,但是为了描述方便,除非是为了描述具体区别,否则后面的内容会互换使用「线程」和「轻量级进程」这两个名词

进程和线程的关系如下图所示:

在这里插入图片描述

在前面的部分提及到的进程调度,实际上更准确来说是线程调度,只不过其中只有一个线程(即主线程),而从线程部分开始,一个进程内就可以有多个线程,所有线程共享一个进程的资源

对于用于CPU调度来说,其看到的只有每一个线程的task_struct,但是进程中可能不止有一个task_struct,所以CPU所能看到的task_struct个数一定是小于等于进程内部总共task_struct的个数

理解页表与虚拟地址到物理地址转换

前面提到线程本质是一个执行流,执行属于自己的代码,但是一个进程内部所有的线程都共用一个虚拟地址空间,此时就需要考虑如何做到不同的线程看到不同的资源

首先了解Linux中的内存管理,在Linux中,操作系统为了更好得将内存和硬盘进行交互,除了从磁盘上读取数据时按照4kb大小读取外,在内存中,写入/读取数据也是按照4kb进行的,而在内存中,这4kb也被称为页框,有了这样的管理方式,就可以尽可能减少内存的碎片,尽管已经可能存在碎片,但是这个碎片只会出现在多个页框内

同样,每一个页框也有自己的属性,操作系统也需要管理所有的页框,即需要对应的结构,对应的结构源码如下:

struct page 
{unsigned long flags;		/* atomic flags, some possiblyupdated asynchronously */atomic_t count;			/* Usage count, see below. */struct list_head list;		/* ->mapping has some page lists. */struct address_space *mapping;	/* The inode (or ...) we belong to. */unsigned long index;		/* Our offset within mapping. */struct list_head lru;		/* Pageout list, eg. active_list;protected by zone->lru_lock !! */union {struct pte_chain *chain;/* Reverse pte mapping pointer.* protected by PG_chainlock */pte_addr_t direct;} pte;unsigned long private;		/* mapping-private opaque data */#if defined(WANT_PAGE_VIRTUAL)void *virtual;			/* Kernel virtual address (NULL ifnot kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};

接着,操作系统可以考虑使用一个数组对所有的页框进行管理,其中的元素都是struct page*,之所以用数组,原因之一是因为数组可以使用下标进行访问,而在C语言的世界里,下标和对应的地址(即起始地址+偏移量)可以相互转换,有了这一点,一旦在页表中通过虚拟地址找到了对应的物理地址,就可以在对应的页框数组中通过下标找到指向指定物理地址对应页框的指针从而访问到对应的属性

也就是说当获取到一个物理地址,那么通过该地址/4kb即可获取到改地址所在页框的起始地址值,通过该地址值根据C语言数组名即为数组的起始地址的特点就可以获取到偏移量计算出下标,进而获取到指定的元素,对应的地址/4kb也可以转化为地址/ 2 12 b y t e 2^{12}byte 212byte

理解了上面的内存管理方式后,接下来就可以深入理解页表是如何进行虚拟地址到物理地址转换的。因为内存是按照4kb进行读写操作的,所以虚拟地址同样也是按照4kb进行,此时的4kb被称为页,在前面Linux进程地址空间提到,页表中主要的字段有:虚拟地址、物理地址、读写权限和是否存在标记,但是实际上页表中可以并不需要虚拟地址字段,因为虚拟地址是从0开始计算的,一直到最大值,所以可以考虑直接将当前的虚拟地址和虚拟地址空间起始地址之间的偏移量作为索引去对应映射的物理地址即可,这样的做法可以在一定程度上减少页表在物理内存中的占用,但是尽管如此,如果页表中仅有物理地址,在32位系统下,每一个物理地址在每一个页表项中占4个字节(32位比特位),而一个虚拟地址空间就有 2 32 2^{32} 232个地址,所以一共需要 2 32 × 4 b y t e = 2 34 = 16 G B 2^{32}\times 4 byte = 2^{34} = 16GB 232×4byte=234=16GB,可以看到单单一个页表就需要占用16GB,远超一个物理内存本身的大小,所以页表不可能直接是上面的形式

在Linux中,页表实际上是一个多级页表,类似于Linux文件系统部分提到的数据块指针,示意图如下:

在这里插入图片描述

在32为系统下,其中,一级页表称为页目录,一共有1024( 2 10 2^{10} 210)项,每一项称为页目录表项,二级页表(或简称页表)每一个页表也有1024项,每一项称为页表项,页目录中每一项中存储的都是对应二级页表的起始地址,所以总共 1024 × 1024 × 4 = 4 M B 1024\times1024\times4=4MB 1024×1024×4=4MB大小,此时当通过虚拟地址定位物理地址时,就直接通过32位地址依次查询,具体步骤如下:

  1. 前10位(作为下标)定位页目录中指定项,此时就可以找到指定的页表
  2. 中间10位(作为下标)定位页表中指定项,此时就可以找到虚拟地址对应的物理页框起始地址
  3. 最后12位(作为偏移量)定位页框中具体的某一个字节的地址,此时就可以通过页框起始地址+偏移量找到指定字节的地址

整体的示意图如下:

在这里插入图片描述

一旦找到一个字节,如果想通过这个字节作为起始值找不同类型的值,就可以通过类型的字节占用获取到完整的值,这就是为什么编译型语言层面需要有变量类型的原因。而之所以知道对应的页框是否被使用只需要通过前20位获取到对应的页框物理地址转换为下标访问对应页框的属性即可

对应的源码如下:

typedef struct { unsigned long pte; } pte_t; // 页表目录
typedef struct { unsigned long pgd; } pgd_t; // 二级页表

最后,在操作系统中,存在一个指针struct task_struct* current,在前面Linux进程状态与进程优先级部分提到这个指针指向的就是当前正在被调度的进程,具体来说就是进程中对应的线程,当CPU调度时,就会有对应的寄存器存储当前current指针中的内容,所以CPU一直都知道当前正在被调度的进程,而在前面提到过MMU寄存器负责将CPU读取到的虚拟地址进行查表找到对应的物理地址,而要找到页表就需要通过CR3寄存器,所以整个过程如下图所示:

在这里插入图片描述

在CPU发展的过程中,查表行为一直都是一个非常高频的行为,所以其效率也是值得关注的,查表的操作本可以交给软件完成,但是软件的速度毕竟没有硬件快,所以最后还是在CPU中集成了一个硬件MMU,实际上,并不是每一次MMU都去查页表获取到物理地址,而是先看缓存中是否存在已经查到指定虚拟地址对应的物理地址,这个缓存也是通过硬件实现的,即TLB寄存器,所以整个过程就变为:查表时,先查看TLB是否存在需要的虚拟地址对应的物理地址,没有就去查表,并将这个映射存储到TLB,如下图所示:

在这里插入图片描述

理解页表标志位

在Linux进程地址空间部分提到过,页表中含有一些标记位,但是上面的理论只提到了物理地址,实际上,因为在32位系统中,一共由 2 32 2^{32} 232个地址,因为每4kb为一个页框,所以总共有 2 32 2 12 = 2 20 \frac{2^{32}}{2^{12}}=2^{20} 212232=220,所以实际上在每一个二级页表项中,表示完所有的页框地址也只需要20位二进制,剩余的12位就可以用来作为标记位

细致理解缺页中断

CPU给MMU的虚拟地址,在TLB和页表都没有找到对应的物理页,该怎么办呢?其实这就是缺页异常Page Fault,它是个由硬件中断触发的可以由软件逻辑纠正的错误。假如目标内存页在物理内存中没有对应的物理页或者存在但无对应权限,CPU就无法获取数据,这种情况下CPU就会报告一个缺页错误。由于CPU没有数据就无法进行计算,CPU罢工了进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler处理,如下图所示:

在这里插入图片描述

缺页中断会交给 PageFaultHandler 处理,其根据缺页中断的不同类型会进行不同的处理:

  1. Hard Page Fault也被称为Major Page Fault,翻译为硬缺页错误/主要缺页错误,这时物理内存中没有对应的物理,需要CPU打开磁盘设备读取到物理内存中,再让MMU建⽴虚拟地址和物理地址的映射
  2. Soft Page Fault也被称为Minor Page Fault,翻译为软缺页错误/次要缺页错误,这时物理内存中是存在对应物理页的,只不过可能是其他进程调入的,发出缺页异常的进程不知道而已,此时MMU只需要建立映射即可,⽆需从磁盘读取写入内存,一般出现在多进程共享内存区域
  3. Invalid Page Fault翻译为无效缺页错误,⽐如进程访问的内存地址越界访问,又比如对空指针解引⽤内核就会报segmentation fault错误中断进程直接挂掉

线程的优点和缺点

对比进程,线程也有对应的优点,具体如下表所示:

特性进程线程
创建代价创建一个新进程的代价较大创建一个新线程的代价较小
上下文切换工作量进程之间的切换需要操作系统做较多的工作,涉及不同的虚拟内存空间线程之间的切换需要操作系统做的工作较少,因为线程共享相同的虚拟内存空间
寄存器内容切换切换时需要保存和恢复所有寄存器的内容切换时需要保存和恢复较少的寄存器内容
缓存机制影响上下文切换会扰乱处理器缓存机制,导致已缓存的内存地址失效,并且TLB会被刷新上下文切换对处理器缓存机制影响较小,TLB不会被刷新
资源占用占用较多系统资源占用较少系统资源
多处理器利用可以利用多处理器,但效率较低能充分利用多处理器的并行处理能力
等待I/O操作在等待慢速I/O操作结束时,无法执行其他计算任务在等待慢速I/O操作结束时,可以执行其他计算任务

在线程的优点中,主要就是寄存器内容切换和缓存机制上,因为进程需要保存代码和数据,切换时需要全部切换,而线程只需要保存部分数据和代码,切换时只需要切换一部分内容,所以切换线程更方便且容易,另外一个影响切换性能的并不是进程本身的内容,而是缓存硬件,例如前面提到的TLB以及CPU中本身存在的Cache,如果是线程,因为页表不需要切换,并且相关的代码也不需要切换,所以在一定程度上节省了刷新缓存硬件的开销,但是进程就必须全部刷新,所以导致进程切换相对于线程就比较耗时

局部性原理(Principle of Locality)是计算机科学中的一个重要概念,特别是在内存管理和缓存设计中。它描述了程序在访问数据时表现出的一种倾向,即程序倾向于访问最近访问过的数据或与其相邻的数据

局部性原理分为两种主要类型:时间局部性和空间局部性:

  • 时间局部性指的是如果一个数据项被访问,那么在不久的将来它很可能再次被访问。这种现象通常出现在循环、递归调用等场景中,因为这些结构会反复访问相同的数据
  • 空间局部性指的是如果一个数据项被访问,那么与之相邻的数据项也很可能很快被访问。这是因为大多数程序在访问数据时,往往会按顺序访问连续存储的数据,如数组元素、结构体成员等

局部性原理的应用

  1. CPU缓存(Cache):

    • 时间局部性:缓存机制利用时间局部性,将最近访问的数据保存在快速缓存中,以便下次访问时能快速获取
    • 空间局部性:缓存通常以块(cache lines)为单位加载数据,这样当一个数据项被加载到缓存时,其邻近的数据也会一并加载进来,提高访问效率
  2. 虚拟内存管理:

    • 操作系统通过分页机制管理内存,页面大小的设计也考虑了局部性原理。较大的页面可以更好地利用空间局部性,但可能会增加内存浪费;较小的页面则更适合精细的内存管理
  3. 预取技术:

    • 现代处理器和存储系统常使用预取技术(prefetching),基于局部性原理预测即将访问的数据,并提前将其加载到缓存或更高层次的存储中,从而减少等待时间
  4. 文件系统和数据库:

    • 文件系统和数据库系统在设计时也会考虑局部性原理,例如通过缓存最近访问的文件块或数据库记录来提高性能

在Linux中查看CPU信息可以使用下面的指令:

cat /proc/cpuinfo

同样,线程也有对应的缺点:

  1. 性能损失:计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,可能会有较大的性能损失,增加额外的同步和调度开销
  2. 健壮性降低:编写多线程需要更全面深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性很大,线程之间缺乏保护
  3. 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
  4. 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多

线程独有和共有的数据

在PCB中,线程独有的数据:

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

线程中共有的数据:

同一地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到除此之外,各线程还共享以下进程资源和环境:

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

线程操作

创建线程与获取线程id

基本使用

如果想在Linux下创建进程可以使用下面的函数:

int pthread_create(pthread_t * thread, const pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);

需要注意,pthread_create函数并不是Linux提供的创建线程的系统调度,而是Linux中对轻量级进程已有的接口进行封装的动态库中的函数,既然是动态库,那么在编译链接时就需要在编译命令后带上-lpthread

需要注意,较新的Linux操作系统可能不需要携带-lpthread,但是对于不需要带上-lpthread的Linux也可以带上,不会影响编译

前面提到在Linux中只存在轻量级进程,即LWP,也就是说在Linux下创建线程就需要使用创建轻量级进程的方式,实际上,在Linux系统调用中存在一个调用clone,该接口可以根据传入的指定标记指定创建的是子进程还是轻量级进程,但是这个接口使用起来非常麻烦,所以为了进一步简化创建线程的步骤使其与通用操作系统中提到的线程概念更加贴切,就出现了用户级线程库,即libpthread.so

该函数中,第一个参数表示创建的新线程的id值,第二个参数表示线程属性,一般情况下用不到,只需要传递NULL即可,第三个参数表示线程需要执行的代码,其为一个回调函数,第四个参数就是回调函数的参数

使用pthread_create创建线程的方式如下:

#include <iostream>
#include <unistd.h>
#include <pthread.h>void *routine(void *arg)
{while (true){std::cout << "我是一个轻量级进程(线程)" << std::endl;sleep(1);}
}int main()
{pthread_t thid;pthread_create(&thid, NULL, routine, (void *)"thread-1");while (true){std::cout << "我是主线程" << std::endl;sleep(1);}return 0;
}

编译运行上面的代码即可看到下面的运行结果:

在这里插入图片描述

从上面的运行结果可以看出,两个线程是异步打印到同一个资源(显示器)上,所以存在打印错乱的情况,但是的确可以做到每一个线程完成自己的任务而不会受到其他线程的干扰,这两个线程就是两个不同的执行流,而因为routine函数始终在访问同一个资源,所以此时其是不可重入函数

理解资源划分

通过上面这个例子来理解操作系统是如何针对每一个线程进行资源划分的。实际上,并不需要操作系统单独采取一套新的方案来处理资源划分的问题,因为在程序编译链接时,每一个函数乃至变量都具有了虚拟地址,一旦CPU拿到了对应的虚拟地址,根据虚拟地址到物理地址的映射即可指定对应的代码得到对应的结果,这种通过部分虚拟地址查找页表的部分区域的方式就可以实现资源划分

线程瓜分进程资源

另外因为一个进程中可能存在很多个线程,所有线程共享整个进程拥有的资源,此时势必存在某些资源是被平均分配的,例如进程的时间片,如果一个进程中存在多个线程,那么多个线程拿到的时间片并不等于进程所有的时间片,而是所有线程拿到的时间片总和等于整个线程的时间片

查看轻量级进程状态与其id

如果此时需要查看轻量级进程的状态,可以使用下面的命令:

ps -aL

例如查看上面运行的进程对应的轻量级进程如下:

在这里插入图片描述

其中,PIDLWP相同的即为主线程,其余的均为新线程,即轻量级进程

在用户级线程库中提供了一个接口pthread_self来获取当前线程的id

pthread_t pthread_self(void);

因为参数是void,所以该接口不需要传递任何参数

需要注意的是,线程id不等于使用ps -aL指令查看到的LWP

创建多线程并且访问同一个函数中的局部变量

若此时有多个新线程,并且均访问同一个方法,当在该函数中使用一个局部变量时,因为每一个新线程都会在自己的栈上开辟栈帧空间,所以这个局部变量在理论上时每一个线程所私有的,即:

#include <iostream>
#include <unistd.h>
#include <pthread.h>void *routine(void *arg)
{int val = 1;while (true){std::cout << "我是" << pthread_self() << "轻量级进程(线程),val = " << val << std::endl;val++;sleep(1);}
}int main()
{pthread_t thid1;pthread_t thid2;pthread_create(&thid1, NULL, routine, (void *)"thread-1");pthread_create(&thid2, NULL, routine, (void *)"thread-1");while (true){std::cout << "我是主线程" << std::endl;sleep(1);}return 0;
}

编译运行上面的代码即可看到尽管两个两个新线程都在修改val,但是二者并没有影响到另外的val,这也验证了每个线程实际上也有自己的栈

创建多线程并且访问同一个全局变量

但是如果两个线程访问的是全局变量,则结果就会不一样:

#include <iostream>
#include <unistd.h>
#include <pthread.h>int val = 0;void *routine(void *arg)
{// int val = 1;while (true){std::cout << "我是" << pthread_self() << "轻量级进程(线程),val = " << val << std::endl;val++;sleep(1);}
}int main()
{pthread_t thid1;pthread_t thid2;pthread_create(&thid1, NULL, routine, (void *)"thread-1");pthread_create(&thid2, NULL, routine, (void *)"thread-1");while (true){std::cout << "我是主线程" << std::endl;sleep(1);}return 0;
}

在上面的代码中,将val变量作为了全局变量供两个线程进行访问,此时两个线程均看得到这个变量,所以这个变量就是两个线程的共享资源,与前面显示器打印问题一样,因为共用并且没加保护,导致一个线程的修改会影响到另一个线程,这个效果同样适用于静态变量

线程局部存储

如果希望每个线程访问全局变量时不受其他线程的影响可以使用线程局部存储,其允许每个线程拥有自己的全局变量副本。这意味着,虽然变量名是全局的,但每个线程访问的是该变量的独立实例

在Linux中,开启线程局部存储可以使用__thread关键字修饰内置类型的全局变量,注意不可以修饰自定义类型。例如下面的代码:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>// 使用线程局部存储
__thread int val = 0;void *routine(void *arg)
{while (true){std::cout << "我是" << pthread_self() << "轻量级进程(线程),我的val=" << val << std::endl;val++;sleep(1);}return arg;
}int main()
{pthread_t thid1;pthread_create(&thid1, NULL, routine, (void *)"thread-1");while (true){std::cout << "我是主线程,我的val=" << val << std::endl;sleep(1);}return 0;
}

运行上面的代码后可以看到下面的结果:

在这里插入图片描述

从上面的结果可以看出,新线程对全局变量val的修改并不影响主线程

理解栈独立和堆共享

是每个线程私有的内存区域,用于存储局部变量、方法参数、返回地址等。每个新线程都会创建自己的栈。栈上的数据独立于其他线程,这意味着如果一个线程在它的栈中创建了一个变量,这个变量不能被直接访问到,除非它被显式地传递给另一个线程(例如通过方法参数或共享对象)。这种独立性提供了天然的线程安全,因为每个线程都有自己的栈空间,不会无意间影响其他线程的数据

是一个全局共享的内存区域,用于动态分配内存。所有线程都可以访问堆上分配的对象。如果一个对象在堆上被创建,并且其引用(即内存地址)被传递给多个线程,那么这些线程就可以同时访问甚至修改该对象的内容。因为堆是共享的,所以在多线程环境下访问堆上的对象时需要特别小心,通常需要使用同步机制来避免竞态条件或数据不一致的问题

所以,不论是堆上的数据还是栈上的数据,只要能获取到指定内容的地址,就不存在所谓的栈独立性,此刻栈和堆都是共享的,而所谓的栈独立性和堆共享性更强调所有的线程都有自己的栈,但是堆只共有一个

信号问题

在一个进程中,不论是哪一个线程出现线程异常问题,例如除0异常、野指针异常等,都会导致整个进程崩溃而不是单独的一个线程退出,所以对于任意一个线程来说,只要是同一个进程,那么一旦有一个线程收到了某一个信号,那么其它的所有线程都会收到同一个信号,从而执行同一个信号处理方法

但是每个线程可以有自己的信号屏蔽字,即每个线程可以有自己的阻塞信号的位图

线程等待与回收

前面学到子进程一旦退出,父进程就要回收对应的子进程,防止僵尸进程问题,同样,对于线程来说,也存在类似于僵尸进程的问题,但是这个问题在操作系统重无法查看,所以一旦线程退出,主线程也需要等待并回收退出的线程。另外,一个线程是否完成任务也需要通过对应线程的退出信息判断,所以在主线程需要通过等待并回收对应的线程从而获取到退出信息

基于上面两点原因,主线程需要等待并回收退出的线程。在Linux中,如果需要等待退出的线程,可以使用接口pthread_join

int pthread_join(pthread_t thread, void **retval);

这个接口的第一个参数表示要等待的线程id,第二个参数是一个输出型参数,线程退出的信息会写入到该参数,后续只需要读取该参数的值即可获取到退出线程的退出信息。之所以第二个参数是二级指针,是因为前面创建线程时对应执行函数的返回值为void*,传入一个变量时同样需要对应大小的变量接收对应的返回值防止出现截断或者溢出问题,而需要修改一个一级指针就需要一个二级指针,示意图如下:

在这里插入图片描述

需要注意,在上图中,线程执行函数的退出信息并不是直接就被pthread_join函数拿到的,而是先存放在线程结构pthread中,再有pthread_join函数通过获取对应的属性拿到线程执行函数的退出信息

如果等待的线程没有退出,那么等待方式依旧是阻塞式等待,一旦等待成功,函数返回0,否则返回指定的错误码

基本使用如下:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>void *routine(void *arg)
{int count = 0;while (true){std::cout << "我是" << pthread_self() << "轻量级进程(线程) " << std::endl;count++;sleep(1);if (count == 5)break;}return arg;
}int main()
{pthread_t thid1;pthread_create(&thid1, NULL, routine, (void *)"thread-1");// 等待线程void *ret = nullptr;int n = pthread_join(thid1, &ret);if (!n)printf("%s\n", (char *)ret);elsestd::cerr << "错误码为:" << n << strerror(n) << std::endl;while (true){sleep(1);}return 0;
}

运行结果如下:

在这里插入图片描述

创建线程的传参问题和回收线程的返回值问题:

观察到不论是创建线程时传入线程函数的参数还是等待回收线程获取线程函数的返回值对应的类型是void*类型的指针,这种指针类型没有任何类型说明,只是为了让编译器方便开辟空间,但是正因为这个类型的存在,可以让传参和返回值为任意类型

线程退出

线程退出类似于前面的进程退出,但是如果线程直接使用exit接口会导致整个进程退出,但是线程使用return只是当前线程退出,类似的还可以使用pthread_exit接口退出当前线程而不是整个进程:

void pthread_exit(void *retval);

该接口接收一个参数表示退出信息

除了上面的两种线程退出的方法外,还可以使用下面的接口取消一个线程:

int pthread_cancel(pthread_t thread);

该接口接收一个参数,表示需要取消的线程id

一般情况下,这个接口是主线程调用,取消其他的新线程,并且必须取消线程之前必须确保指定的线程存在

注意,取消的线程依旧需要回收,并且对应线程的退出码为-1,这个值是一个宏,定义如下:

#define PTHREAD_CANCELED ((void*)-1)

线程分离

前面回收线程时是阻塞式等待,但是注意回收线程没有非阻塞式等待,所以也就没有对应的非阻塞式等待的接口。在Linux中,线程被等待的状态分为以下两种:

  1. joinable:表示线程需要被阻塞式等待
  2. detached:表示线程已经被分离,主线程不再需要等待

需要注意的是,在多执行流的情况下,一定要确保主执行流最后退出,防止分离的线程比主执行流先退出导致分离的线程需要的资源被释放导致错误

线程分离操作可以分为两种:

  1. 自主分离
  2. 被动分离

需要分离线程时可以使用下面的接口:

int pthread_detach(pthread_t thread);

该接口传递一个参数,表示需要分离的线程的id,如果是自主分离,则通过pthread_self接口获取到当前线程的id实现,否则通过直接通过具体的值被其他线程被动分离

由于分离后的新线程不需要主线程等待,所以如果一个线程依旧等待分离的线程,那么pthread_join就不再返回0而是返回22

线程程序替换

注意,进程程序替换是不可以直接发生在线程执行流中的,因为程序替换的本质是替换掉当前进程的代码,此时线程的代码也会被替换从而导致错误,但是可以在线程中创建子进程,再在子进程中使用进程程序替换

C++中的线程操作

除了C语言对系统的相关线程操作进行了封装外,其他编程语言也会做同样的事情,这样可以确保语言具有可移植性,同样,C++中也有对应的线程库<thread>,具体接口见C++线程库部分,但是需要注意的是,如果是在Linux下使用C++线程库编写代码,编译该代码时依旧需要携带-lpthread,因为C++线程库本质也是封装了libpthread.so

相关文章:

Linux线程概念与线程操作

Linux线程概念与线程操作 线程概念 前面提到进程程序代码和数据进程结构体&#xff0c;在线程部分就需要进一步更新之前的认识 进程实际上承担分配系统资源的基本实体&#xff0c;而线程是进程中的一个执行分支&#xff0c;是操作系统调度的基本单位 此处需要注意&#xff0…...

AI软件栈:LLVM分析(五)

数据流分析是编译优化、代码生成的关键理论。其数学基础是离散数学中的半格(Semi-Lattice)和格。半格与格不仅是编译优化和代码生成的重要理论基础,也是程序分析、验证及自动化测试的系统理论基础。 文章目录 格、半格与不动点格、半格与不动点 半格是指针对二元组 < S …...

Git指南-从入门到精通

代码提交和同步命令 流程图如下&#xff1a; 第零步: 工作区与仓库保持一致第一步: 文件增删改&#xff0c;变为已修改状态第二步: git add &#xff0c;变为已暂存状态 bash $ git status $ git add --all # 当前项目下的所有更改 $ git add . # 当前目录下的所有更改 $ g…...

Linux 文件系统挂载

系列文章目录 Linux内核学习 Linux 知识&#xff08;1&#xff09; Linux 知识&#xff08;2&#xff09; WSL Ubuntu QEMU 虚拟机 Linux 调试视频 PCIe 与 USB 的补充知识 vscode 使用说明 树莓派 4B 指南 设备驱动畅想 Linux内核子系统 Linux 文件系统挂载 文章目录 系列文章…...

Qt QSpinBox 总结

Qt5 QSpinBox 总结 1. 基本特性 用途&#xff1a;用于输入和调整整数值&#xff0c;支持通过上下箭头、键盘输入或编程方式修改值。 默认范围&#xff1a;0 到 99&#xff0c;可通过 setRange(min, max) 自定义。 步长控制&#xff1a;setSingleStep(step) 设置单步增减值&a…...

【OJ项目】深入剖析题目接口控制器:功能、实现与应用

《深入剖析题目接口控制器&#xff1a;功能、实现与应用》 一、引言 在在线编程平台或竞赛系统中&#xff0c;题目管理和提交是核心功能之一。QuestionController 类作为控制器层&#xff0c;承担着处理与题目相关的各种请求的重要职责&#xff0c;包括题目的增删改查、题目提…...

周考考题(学习自用)

1.查询student表中name叫张某的信息 select * from student where name张某; 2.写出char和varchar类型的区别 1&#xff09;char存储固定长度的字符串&#xff0c;varchar存储可变长度的字符串&#xff08;在实际长度的字符串上加上一个字节用于存储字符串长度&#xff09;&a…...

【matlab】大小键盘对应的Kbname

matlab中可以通过Kbname来识别键盘上的键。在写范式的时候&#xff0c;遇到一个问题&#xff0c;我想用大键盘上排成一行的数字按键评分&#xff0c;比如 Kbname(1) 表示键盘上的数字1&#xff0c;但是这种写法只能识别小键盘上的数字&#xff0c;无法达到我的目的&#xff0c;…...

LabVIEW与小众设备集成

在LabVIEW开发中&#xff0c;当面临控制如布鲁克OPUS红外光谱仪这类小众专业设备的需求&#xff0c;而厂家虽然提供了配套软件&#xff0c;但由于系统中还需要控制其他设备且不能使用厂商的软件时&#xff0c;必须依赖特定方法通过LabVIEW实现设备的控制。开发过程中&#xff0…...

Android 系统Service流程

主要用到的源码文件 /frameworks/base/core/java/android/app/ContextImpl.java 和ams通信。 /frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java 初始化Service,.管理服务 ActiveServices对象mServices /frameworks/base/services/core/…...

Gartner预测2025年网络安全正在进入AI动荡时期:软件供应链和基础设施技术堆栈中毒将占针对企业使用的人工智能恶意攻击的 70% 以上

Gartner 预测&#xff0c;网络安全正在进入 AI 动荡时期。安全和风险管理领导者必须根据早期生成式 AI 部署的失败以及 AI 代理清洗来评估即将到来的 AI 进展。 主要发现 随着各大企业开展大量人工智能采用和开发项目&#xff0c;应用安全弱点的暴露程度不断提高&#xff0c;包…...

华为最新OD机试真题-最长子字符串的长度(一)-Python-OD统一考试(E卷)

最新华为OD机试考点合集:华为OD机试2024年真题题库(E卷+D卷+C卷)_华为od机试题库-CSDN博客 每一题都含有详细的解题思路和代码注释,精编c++、JAVA、Python三种语言解法。帮助每一位考生轻松、高效刷题。订阅后永久可看,发现新题及时跟新。 题目描述: 给你一个字符串…...

HAL库框架学习总结

概述&#xff1a;HAL库为各种外设基本都配了三套 API&#xff0c;查询&#xff0c;中断和 DMA。 一、HAL库为外设初始化提供了一套框架&#xff0c;这里以串口为例进行说明&#xff0c;调用函数 HAL_UART_Init初始化串口&#xff0c;此函数就会调用 HAL_UART_MspInit&#xff0…...

基于Spring Integration的ESB与Kettle结合实现实时数据处理技术

一、方案概述 在当今数字化时代,企业面临着海量数据的实时处理与传输挑战。ESB(企业服务总线)作为系统集成的核心组件,承担着不同协议数据的接入与转换任务,而Kettle作为一款功能强大的ETL(Extract, Transform, Load)工具,在数据抽取、转换与加载方面表现出色。将ESB与…...

qt QOpenGLContext详解

1. 概述 QOpenGLContext 是 Qt 提供的一个类&#xff0c;用于管理 OpenGL 上下文。它封装了 OpenGL 上下文的创建、配置和管理功能&#xff0c;使得开发者可以在 Qt 应用程序中以平台无关的方式使用 OpenGL。通过 QOpenGLContext&#xff0c;可以轻松地创建和管理 OpenGL 上下…...

探索顶级汽车软件解决方案:驱动行业变革的关键力量

在本文中&#xff0c;将一同探索当今塑造汽车行业的最具影响力的软件解决方案。从设计到制造&#xff0c;软件正彻底改变车辆的制造与维护方式。让我们深入了解这个充满活力领域中的关键技术。 设计软件&#xff1a;创新车型的孕育摇篮 车辆设计软件对于创造创新型汽车模型至…...

Deepseek R1模型本地化部署+API接口调用详细教程:释放AI生产力

文章目录 前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装ollama2部署DeepSeek R1模型删除已存在模型&#xff0c;以7b模型为例 三、DeepSeek API接口调用Cline配置 前言 随着最近人工智能 DeepSeek 的爆火&#xff0c;越来越多的技术大佬们开始关注如…...

DeepSeek 概述与本地化部署【详细流程】

目录 一、引言 1.1 背景介绍 1.2 本地化部署的优势 二、deepseek概述 2.1 功能特点 2.2 核心优势 三、本地部署流程 3.1 版本选择 3.2 部署过程 3.2.1 下载Ollama 3.2.2 安装Ollama 3.2.3 选择 r1 模型 3.2.4 选择版本 3.2.5 本地运行deepseek模型 3.3.6 查看…...

FFmpeg Video options

FFmpeg视频相关选项 1. -vframes number (output) 设置输出视频帧数 示例&#xff1a; ffmpeg -i input.mp4 -vframes 90 output.mp4 表示输出90帧视频 2. -r[:stream_specifier] fps (input/output,per-stream) 设置帧率(rate) 示例&#xff1a; ffmpeg -i input.mp4…...

从51到STM32:PWM平滑迁移方案

引言 对于习惯使用51单片机的开发者而言&#xff0c;转向STM32时可能会面临开发环境和硬件差异的挑战。本文以PWM&#xff08;脉宽调制&#xff09;功能为例&#xff0c;分享从51到STM32的平滑迁移方案&#xff0c;帮助开发者快速适应STM32的开发模式。 一、PWM实现原理对比 …...

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇&#xff0c;在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下&#xff1a; 【Note】&#xff1a;如果你已经完成安装等操作&#xff0c;可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作&#xff0c;重…...

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

Java 语言特性(面试系列2)

一、SQL 基础 1. 复杂查询 &#xff08;1&#xff09;连接查询&#xff08;JOIN&#xff09; 内连接&#xff08;INNER JOIN&#xff09;&#xff1a;返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

【kafka】Golang实现分布式Masscan任务调度系统

要求&#xff1a; 输出两个程序&#xff0c;一个命令行程序&#xff08;命令行参数用flag&#xff09;和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽&#xff0c;然后将消息推送到kafka里面。 服务端程序&#xff1a; 从kafka消费者接收…...

智慧医疗能源事业线深度画像分析(上)

引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

1688商品列表API与其他数据源的对接思路

将1688商品列表API与其他数据源对接时&#xff0c;需结合业务场景设计数据流转链路&#xff0c;重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点&#xff1a; 一、核心对接场景与目标 商品数据同步 场景&#xff1a;将1688商品信息…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》

在注意力分散、内容高度同质化的时代&#xff0c;情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现&#xff0c;消费者对内容的“有感”程度&#xff0c;正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中&#xff0…...

使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装

以下是基于 vant-ui&#xff08;适配 Vue2 版本 &#xff09;实现截图中照片上传预览、删除功能&#xff0c;并封装成可复用组件的完整代码&#xff0c;包含样式和逻辑实现&#xff0c;可直接在 Vue2 项目中使用&#xff1a; 1. 封装的图片上传组件 ImageUploader.vue <te…...

GitHub 趋势日报 (2025年06月08日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...

NFT模式:数字资产确权与链游经济系统构建

NFT模式&#xff1a;数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新&#xff1a;构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议&#xff1a;基于LayerZero协议实现以太坊、Solana等公链资产互通&#xff0c;通过零知…...