【多线程】二、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
,可以在多种操作系统上使用,包括 Linux
、UNIX
、MacOS
等。
POSIX
库提供了一系列的函数来创建、同步和销毁线程,绝大多数函数的名字都是以 pthread_
打头的,比如 pthread_create()
、pthread_join()
、pthread_mutex_init()
、pthread_cond_wait()
等等。这些函数可以帮助程序员实现线程的创建、同步和协作等操作。
二、调用前提🎍
- 要使用这些函数库,必须引入头文件
<pthread.h>
- 使用
gcc
等编译器链接这些线程函数库时,要使用编译器命令的-lpthread
选项
三、用户态线程和内核级线程的区别
【用户级线程】和【内核级线程】的主要区别在于它们的执行上下文和调度方式。
【用户级线程】是由用户空间的线程库实现的,它们的执行上下文(寄存器、栈、程序计数器等)保存在用户进程的虚拟地址空间中,因此线程的切换是由线程库完成的。用户态线程的调度也是由线程库完成的,它们被调度时只能在当前进程的上下文中运行,因此如果某个用户态线程执行了一个耗时的操作,它可能会阻塞整个进程。
【内核级线程】是由操作系统内核实现和管理的,它们的执行上下文保存在内核空间中,因此线程的切换是由内核完成的。内核级线程的调度也是由内核完成的,它们被调度时可以在任何进程的上下文中运行,因此一个内核级线程阻塞不会影响其他线程的执行。
在一些系统中,用户态线程和内核级线程可能存在一定的混合模式,比如 Linux
中的轻量级进程。轻量级进程在内核中被表示为内核级线程,但是由用户态线程库来创建和管理,因此轻量级进程的执行上下文保存在用户进程的虚拟地址空间中,线程的切换也是由线程库完成的。但是由于轻量级进程是由内核来调度的,因此它们的调度方式与内核级线程相似,可以在任何进程的上下文中运行。
除了上述区别之外,用户态线程和内核级线程还有以下区别:
- 调度方式:内核级线程的调度是由内核来完成的,而用户态线程的调度则是由用户程序自己来完成的。
- 系统调用:内核级线程在进行系统调用时会切换到内核态,而用户态线程在进行系统调用时会导致整个进程被阻塞,因为用户态线程是没有直接访问系统调用接口的权限的,需要通过系统调用将控制权交给内核来完成。
- 线程切换:内核级线程在进行线程切换时,需要保存和恢复整个进程的状态,开销较大。而用户态线程在进行线程切换时,只需要保存和恢复当前线程的状态,开销较小。
- 调试和性能分析:由于内核级线程是由内核来管理和调度的,因此在进行调试和性能分析时,需要使用内核级工具来进行分析。而用户态线程则可以使用用户态工具进行分析。
总之,用户态线程和内核级线程都有各自的优缺点,选择何种方式取决于应用的具体情况。
Ⅱ. 线程控制
一、创建线程pthread_create()
// 功能:创建一个新线程
// 返回值:成功返回0;失败返回错误码,并且*thread的值是未定义的
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:传给线程启动函数也就是
start_routine
的参数。
其中 pthread_t
和 pthread_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
选项可以 以全格式显示进程信息,包括进程的 UID
、PID
、PPID
、LWP
、C
、NLWP
、STIME
、TTY
、TIME
、CMD
等信息。
💥💥💥注意:ps -L
命令中显示的 LWP ID
是在内核态中分配的轻量级进程的 ID
。
下面我们通过监控脚本来观察一下:
while :; do ps -afL | head -1 && ps -afL | grep mythread; sleep 1; done
可以看到上述结果中,两个线程的 PID
都是相同的,也就是进程 ID
相同,但是它们的 LWP
是不同的,线程之间就是用 LWP
来区分不同的线程的!
除此之外,可以看到 主线程的 LWP
与 PID
是一致的,都是 28179
,这样子也验证了我们以前在讲进程的时候,单线程的进程的 PID
和 LWP
也是相同的,完全不违背我们现在所学的知识!
除此之外,还可以看到 NLWP
的大小为 2
,代表当前这个进程内一共有两个线程,也就是两个执行流!总结下来就是:
- LWP:代表线程ID,既
gettid()
系统调用的返回值。 - NLWP:代表线程组内线程的个数
gettid()
和 线程组 下面会讲到,别急这里开始的知识点比较绕,阅读时候一定要仔细思考,想清楚了就不难了~!
☢️ 线程ID 与 进程ID
上面我们通过 ps -aL
指令可以看到线程的 PID
和 LWP
,PID
我们知道这是进程的一个标识符,那这个 LWP
呢❓❓❓
其实这里的 LWP
并不是全称,这里表达的意思是:LWP ID
,也就是轻量级进程的 ID
,每个轻量级进程都被内核赋予了一个唯一的 LWP ID
,用于标识不同的轻量级进程。
LWP ID
与 线程ID
是等价的,它们指向同一个轻量级进程。
❗除此之外,在 Linux
中,轻量级进程 LWP
的 ID
确实是在内核中分配和管理的。在用户态中,轻量级进程是由 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
的区别如下表所示:
用户态 | 系统调用 | 内核进程描述符中对应的数据 |
---|---|---|
进程ID | pid_t getpid() | pid_t tgid |
线程ID | pid_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