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

【多线程】二、pthread库 线程控制 线程分离 __thread关键字 线程库封装

文章目录

  • Ⅰ. 轻量级线程库
    • 一、POSIX线程库 -- pthread库
    • 二、调用前提🎍
    • 三、用户态线程和内核级线程的区别
  • Ⅱ. 线程控制
    • 一、创建线程pthread_create()
    • 🛺 ps命令的 -L 选项
    • ☢️ 线程ID 与 进程ID
    • 🎏 线程组的引入
    • 🧨 线程库 && 线程库进程地址空间布局
    • 二、获取用户态线程ID
    • 🎏 创建多个线程的情况
    • 三、线程终止
      • ① pthread_exit() 函数
      • ② pthread_cancel() 函数
    • 四、线程等待pthread_join()
  • Ⅲ. 线程分离detach()
  • Ⅳ. 拓展:__thread关键字
  • Ⅴ. 基于线程操作封装一个线程库

在这里插入图片描述

Ⅰ. 轻量级线程库

​ 在讲线程控制之前,我们必须先有一些基本认识的搭建!

​ 我们之前讲过,在 linux 中是没有所谓的线程的,而是用一种 “轻量级进程” 来代替,但是我们以前创建新进程的时候,是通过 fork() 函数来实现的,难道我们创建线程的时候也要调用 fork() 吗❓❓❓

​ 当然不是,但是 操作系统只能给我们提供创建轻量级进程的接口。为了解决 线程“轻量级进程“ 之间的问题,可以说是概念不匹配的问题,而引入了多种原生线程库,这些库为我们提供了一系列的线程接口,让我们可以在 用户态 就能实现控制线程。

一、POSIX线程库 – pthread库

POSIX 线程库 POSIX threads,简称 pthread 是一种标准的线程库,它实现了 POSIX 标准定义的线程 API,可以在多种操作系统上使用,包括 LinuxUNIXMacOS 等。

POSIX 库提供了一系列的函数来创建、同步和销毁线程,绝大多数函数的名字都是以 pthread_ 打头的,比如 pthread_create()pthread_join()pthread_mutex_init()pthread_cond_wait() 等等。这些函数可以帮助程序员实现线程的创建、同步和协作等操作。

二、调用前提🎍

  • 要使用这些函数库,必须引入头文件 <pthread.h>
  • 使用 gcc 等编译器链接这些线程函数库时,要使用编译器命令的 -lpthread 选项

三、用户态线程和内核级线程的区别

​ 【用户级线程】和【内核级线程】的主要区别在于它们的执行上下文和调度方式。

​ 【用户级线程】是由用户空间的线程库实现的,它们的执行上下文(寄存器、栈、程序计数器等)保存在用户进程的虚拟地址空间中,因此线程的切换是由线程库完成的。用户态线程的调度也是由线程库完成的,它们被调度时只能在当前进程的上下文中运行,因此如果某个用户态线程执行了一个耗时的操作,它可能会阻塞整个进程。

​ 【内核级线程】是由操作系统内核实现和管理的,它们的执行上下文保存在内核空间中,因此线程的切换是由内核完成的。内核级线程的调度也是由内核完成的,它们被调度时可以在任何进程的上下文中运行,因此一个内核级线程阻塞不会影响其他线程的执行。

​ 在一些系统中,用户态线程和内核级线程可能存在一定的混合模式,比如 Linux 中的轻量级进程。轻量级进程在内核中被表示为内核级线程,但是由用户态线程库来创建和管理,因此轻量级进程的执行上下文保存在用户进程的虚拟地址空间中,线程的切换也是由线程库完成的。但是由于轻量级进程是由内核来调度的,因此它们的调度方式与内核级线程相似,可以在任何进程的上下文中运行。

除了上述区别之外,用户态线程和内核级线程还有以下区别:

  1. 调度方式:内核级线程的调度是由内核来完成的,而用户态线程的调度则是由用户程序自己来完成的。
  2. 系统调用:内核级线程在进行系统调用时会切换到内核态,而用户态线程在进行系统调用时会导致整个进程被阻塞,因为用户态线程是没有直接访问系统调用接口的权限的,需要通过系统调用将控制权交给内核来完成。
  3. 线程切换:内核级线程在进行线程切换时,需要保存和恢复整个进程的状态,开销较大。而用户态线程在进行线程切换时,只需要保存和恢复当前线程的状态,开销较小。
  4. 调试和性能分析:由于内核级线程是由内核来管理和调度的,因此在进行调试和性能分析时,需要使用内核级工具来进行分析。而用户态线程则可以使用用户态工具进行分析。

总之,用户态线程和内核级线程都有各自的优缺点,选择何种方式取决于应用的具体情况。

Ⅱ. 线程控制

一、创建线程pthread_create()

// 功能:创建一个新线程
// 返回值:成功返回0;失败返回错误码,并且*thread的值是未定义的
int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);

参数如下:

  • thread:存储线程 ID,也就是 线程标识符
  • attr:设置线程的属性,attrNULL 表示使用默认属性
  • start_routine:函数指针,代表线程启动后要执行的函数。
  • arg:传给线程启动函数也就是 start_routine 的参数。

​ 其中 pthread_tpthread_attr_t 的声明如下:

typedef unsigned long int pthread_t; // 用于存放线程ID的union pthread_attr_t
{char __size[__SIZEOF_PTHREAD_ATTR_T]; // __SIZEOF_PTHREAD_ATTR_T其实就是一个宏,为56long int __align;
};

​ 💥💥注意:pthread.h 头文件中的 pthread_t 类型在定义时被 typedef 为无符号长整型 unsigned long int,这样做是为了向后兼容早期的 POSIX 标准和老旧的系统实现。在一些旧的 POSIX 实现中,pthread_t 类型可能是一个无符号长整型,而不是一个不透明的结构体类型。

​ 然而,在现代的 Linux 系统和标准 POSIX 线程库实现中,pthread_t 类型被定义为一个不透明的结构体类型。应用程序应该使用 POSIX 线程库提供的函数来操作 pthread_t 类型的值,而不是直接访问或修改它的内部成员。因此,虽然 pthread_t 类型在定义时被 typedef 为无符号长整型,但实际上它是一个不透明的结构体类型。


​ 下面我们写一段代码来创建新线程:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cassert>
using namespace std;// 新线程执行的函数
void* thread_routine(void* arg)
{char* mes = static_cast<char*>(arg);while(true){cout << "我是新线程,我正在运行:" << mes << endl;sleep(1);}
}int main()
{pthread_t thread;// 创建新线程int n = pthread_create(&thread, nullptr, thread_routine, (void*)"new thread");assert(n == 0);static_cast<void>(n); // 防止realse下报错// 主线程执行while(true){cout << "我是主线程,我正在运行" << endl;sleep(1);}return 0;
}

在这里插入图片描述

​ 这是一个刚接触线程编程经常遇到的一个问题,我们在上面讲过了,我们调用的 pthread_create() 以及后面我们所学的线程接口,其实都不是操作系统提供的,而是应用层提供的,所以我们 在编译的时候必须显式的加上 -lpthread 选项表示链接线程库才行!

在这里插入图片描述

​ 可以看到,成功链接线程库后,跑出来的结果就是我们的主线程和新线程两个线程在跑,那么我们可以用什么方式来观察两个线程呢❓❓❓

🛺 ps命令的 -L 选项

ps -a 命令是用来列出系统上所有进程的详细信息,包括它们的 PID(进程ID)、PPID(父进程ID)、状态(R表示正在运行,S表示休眠,Z表示僵尸进程等)、CPU利用率内存占用等信息。

​ 其中,加上 -L 查看轻量级进程的信息。如果不加 -L 选项,则只列出进程的主线程。这个命令可以帮助我们了解系统上运行的所有进程和线程,方便进行进程和线程的调试和管理。

​ 另外,加上 ps 命令的 -f 选项可以 以全格式显示进程信息,包括进程的 UIDPIDPPIDLWPCNLWPSTIMETTYTIMECMD 等信息。

​ 💥💥💥注意:ps -L 命令中显示的 LWP ID 是在内核态中分配的轻量级进程的 ID

​ 下面我们通过监控脚本来观察一下:

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

在这里插入图片描述

​ 可以看到上述结果中,两个线程的 PID 都是相同的,也就是进程 ID 相同,但是它们的 LWP 是不同的,线程之间就是用 LWP 来区分不同的线程的!

​ 除此之外,可以看到 主线程的 LWPPID 是一致的,都是 28179,这样子也验证了我们以前在讲进程的时候,单线程的进程的 PIDLWP 也是相同的,完全不违背我们现在所学的知识!

​ 除此之外,还可以看到 NLWP 的大小为 2,代表当前这个进程内一共有两个线程,也就是两个执行流!总结下来就是:

  • LWP:代表线程ID,既 gettid() 系统调用的返回值。
  • NLWP:代表线程组内线程的个数

gettid()线程组 下面会讲到,别急这里开始的知识点比较绕,阅读时候一定要仔细思考,想清楚了就不难了~!

☢️ 线程ID 与 进程ID

​ 上面我们通过 ps -aL 指令可以看到线程的 PIDLWPPID 我们知道这是进程的一个标识符,那这个 LWP 呢❓❓❓

​ 其实这里的 LWP 并不是全称,这里表达的意思是:LWP ID,也就是轻量级进程的 ID每个轻量级进程都被内核赋予了一个唯一的 LWP ID,用于标识不同的轻量级进程。

LWP ID线程ID 是等价的,它们指向同一个轻量级进程。

​ ❗除此之外,在 Linux 中,轻量级进程 LWPID 确实是在内核中分配和管理的。在用户态中,轻量级进程是由 POSIX 线程库创建和管理的,而轻量级线程库则负责将 POSIX 线程映射到内核的 LWP 上,从而将用户态线程转换为轻量级进程。因此,LWP ID 的管理是由内核负责的。

​ 所以我们通过 ps -L 看到的 LWP内核级别的线程 ID

🎏 线程组的引入

​ 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖 N 个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了 1:N 关系,POSIX 标准又要求进程内的所有线程调用 getpid 函数时返回相同的进程ID,如何解决上述问题呢?

​ 这个时候 Linux 内核引入了 线程组 的概念:

多线程的进程(又被称为线程组),线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct与之对应。进程描述符结构体 task_struct 中的 pid,表面上看对应的是 进程ID,其实它对应的是 线程ID 也就是 LWP ID。进程描述符 task_struct 中的 tgid,含义是 Thread Group ID,该值对应的是用户层面的 进程ID,也就是咱们之前学进程的时候其实接触的一直都是这个 tgid,就是我们认为的进程ID

struct task_struct 
{pid_t pid;              // 进程或线程的ID,更具体的说法是轻量级进程的IDpid_t tgid;             // 轻量级进程组ID,线程的tgid等于进程的pidstruct list_head tasks;  // 双向链表,用于管理进程或线程struct mm_struct *mm;    // 进程或线程的地址空间信息struct task_struct *parent;  // 父进程的task_struct结构体指针struct files_struct *files;  // 进程或线程的文件描述符表struct signal_struct *signal;  // 进程或线程的信号处理器信息// ... 其他成员,如进程或线程的状态、优先级等信息
};

​ 上面进程 ID 与线程 ID 的区别如下表所示:

用户态系统调用内核进程描述符中对应的数据
进程IDpid_t getpid()pid_t tgid
线程IDpid_t gettid()pid_t pid

​ 对于 gettid() 的调用,其实我们不是直接使用这个接口,只不过这个接口名字会比较好认,真正调用的是 syscall() 接口来获取!

#include <unistd.h>
#include <sys/syscall.h>
int syscall(int number, ...);// 作用:间接调用系统调用
// 返回值:由正在调用的系统调用定义。通常,返回值为0表示成功;-1返回值表示错误,错误代码存储在errno中。

​ 为了能够获得内核的线程 ID,我们需要给 syscall() 接口传递一个参数:SYS_gettid

​ 下面我们还是拿上面的代码,只不过我们这次打印出内核的线程 ID 来验证一下:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/syscall.h> // 调用syscall需要引入该头文件
#include <cassert>
using namespace std;// 新线程执行的函数
void* thread_routine(void* arg)
{char* mes = static_cast<char*>(arg);while(true){cout << "LWP: " << syscall(SYS_gettid) << " 我是新线程,我正在运行:" << mes << endl;sleep(1);}
}int main()
{pthread_t tid;// 创建新线程int n = pthread_create(&tid, nullptr, thread_routine, (void*)"new thread");assert(n == 0);static_cast<void>(n); // 防止realse下报错// 主线程执行while(true){cout << "LWP: " << syscall(SYS_gettid) << " 我是主线程,我正在运行" << endl;sleep(2);}return 0;
}

在这里插入图片描述

​ 💥💥注意:gettid() 函数获取的是内核级别轻量级进程的ID,这和后面我们讲的 pthread_self() 并不一样,pthread_self() 获取的是用户态线程的ID,两者并不相同!且 gettid() 是系统调用,而 pthread_self() 是线程库提供的第三方接口

​ 这个我们下面讲 pthread_create() 的返回值就能理解它们的不同之处了!

​ ☢️☢️强调一点:线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。 不存在父线程子线程的说法!

🧨 线程库 && 线程库进程地址空间布局

​ 还记得我们上面调用的 pthread_create() 中第一个参数类型 pthread_t 吗,我们说过,它可能是一个无符号长整形或者一个结构体,在这里它是一个无符号长整形,接下来我们将它打印出来,看看它到底是个什么东西:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cassert>
using namespace std;// 新线程执行的函数
void* thread_routine(void* arg)
{while
http://www.xdnf.cn/news/100081.html

相关文章:

  • skynet.cluster 库函数应用
  • update方法
  • Kafka 保证多分区的全局顺序性的设计方案和具体实现
  • 接口访问数据库报错问题记录
  • Java多线程的暗号密码:5分钟掌握wait/notify
  • 大模型框架技术演进与全栈实践指南
  • 57、Spring Boot 最佳实践
  • 模板方法模式:定义算法骨架的设计模式
  • 图文结合 - 光伏系统产品设计PRD文档 -(慧哥)慧知开源充电桩平台
  • docker学习笔记5-docker中启动Mysql的最佳实践
  • SQL技术终极指南:从内核原理到超大规模应用
  • 4.23刷题记录(栈与队列专题)
  • devops自动化容器化部署
  • 【人工智能】解锁 AI 潜能:DeepSeek 大模型迁移学习与特定领域微调的实践
  • MCP 协议:AI 时代的 “USB-C” 革命——从接口统一到生态重构的技术哲学
  • 硬核解析:整车行驶阻力系数插值计算与滑行阻力分解方法论
  • vue项目打包后点击dist下面index.html(无法访问您的文件该文件可能已被移至别处、修改或删除。ERR_FILE_NOT_FOUND)比如若依
  • 金仓读写分离集群修改IP
  • 从性能到安全:大型网站系统架构演化的 13 个核心维度
  • Qt案例 使用QFtpServerLib开源库实现Qt软件搭建FTP服务器,使用QFTP模块访问FTP服务器
  • C语言中小写字母转大写字母
  • 数据通信学习笔记之OSPF的基础术语
  • 有哪些信誉良好的脂多糖供应商推荐?
  • 16.第二阶段x64游戏实战-分析二叉树结构
  • 前端js需要连接后端c#的wss服务
  • python自动化测试1——鼠标移动偏移与移动偏移时间
  • Redis 服务自动开启
  • Linux——进程优先级/切换/调度
  • Elasticsearch 堆内存使用情况和 JVM 垃圾回收
  • Maven 项目中引入本地 JAR 包