
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [4 ~> Linux线程 VS 进程](#4 ~> Linux线程 VS 进程)
-
- [4.1 哪些资源是线程独占的?](#4.1 哪些资源是线程独占的?)
- [4.2 进程的多个线程分享](#4.2 进程的多个线程分享)
- [4.3 关于进程线程的问题](#4.3 关于进程线程的问题)
- [5 ~> 线程控制](#5 ~> 线程控制)
-
- [5.1 使用clone,创建LWP](#5.1 使用clone,创建LWP)
- [5.2 pthread库](#5.2 pthread库)
-
- [5.2.1 原生线程库](#5.2.1 原生线程库)
- [5.2.2 pthread的库](#5.2.2 pthread的库)
-
- [5.2.2.1 为什么要"明知不需要,却依然带上 -lpthread"的深层原因](#5.2.2.1 为什么要“明知不需要,却依然带上 -lpthread”的深层原因)
- [5.3 pthread_create接口:几个参数和返回值](#5.3 pthread_create接口:几个参数和返回值)
-
- [5.3.1 参数](#5.3.1 参数)
- [5.3.2 返回值](#5.3.2 返回值)
- [5.3.3 思维导图](#5.3.3 思维导图)
- [5.4 进程、线程叫法区分](#5.4 进程、线程叫法区分)
- [5.5 第一件事:验证线程ID](#5.5 第一件事:验证线程ID)
-
- [5.5.1 修改代码](#5.5.1 修改代码)
- [5.5.2 结论:线程ID的本质其实是一个虚拟地址!](#5.5.2 结论:线程ID的本质其实是一个虚拟地址!)
- [5.5.3 库内部需要对线程做管理吗?](#5.5.3 库内部需要对线程做管理吗?)
- [5.5.4 给线程传递任意类型参数](#5.5.4 给线程传递任意类型参数)
- [5.6 POSIX线程库](#5.6 POSIX线程库)
- [5.7 操作:创建线程](#5.7 操作:创建线程)
-
- [5.7.1 pthread_create](#5.7.1 pthread_create)
- [5.7.2 pthread_self](#5.7.2 pthread_self)
- [5.7.3 创建多线程,给多线程传参](#5.7.3 创建多线程,给多线程传参)
- [5.7.4 构建任务:可以传递C++中的类对象](#5.7.4 构建任务:可以传递C++中的类对象)
-
- [**5.7.4.1 核心逻辑:把线程"装进"对象里**](#5.7.4.1 核心逻辑:把线程“装进”对象里)
- [5.7.4.2 知识点代码拆解:核心实现](#5.7.4.2 知识点代码拆解:核心实现)
- [5.7.4.3 运行过程与底层原理](#5.7.4.3 运行过程与底层原理)
- [5.7.4.4 总结:逻辑严密性检查](#5.7.4.4 总结:逻辑严密性检查)
- [5.7.5 使用PS命令查看线程信息](#5.7.5 使用PS命令查看线程信息)
- [5.8 线程终止](#5.8 线程终止)
-
- [5.8.1 pthread_exit函数](#5.8.1 pthread_exit函数)
- [5.8.2 pthread_cancel函数](#5.8.2 pthread_cancel函数)
- [5.9 线程等待](#5.9 线程等待)
-
- [5.9.1 技术板块:pthread_join 线程退出状态回收](#5.9.1 技术板块:pthread_join 线程退出状态回收)
- [5.9.2 深度逻辑:为什么参数是二级指针 `void **`?](#5.9.2 深度逻辑:为什么参数是二级指针
void **?) - [5.9.3 代码实现参考](#5.9.3 代码实现参考)
- [5.9.4 样例代码](#5.9.4 样例代码)
- [5.10 线程分离](#5.10 线程分离)
-
- [5.10.1 pthread_detach](#5.10.1 pthread_detach)
- [5.10.2 joinable](#5.10.2 joinable)
- [5.10.3 操作(主要留意上面的实验,这个看过就行)](#5.10.3 操作(主要留意上面的实验,这个看过就行))
- [5.10.4 线程分离的两种实现方式](#5.10.4 线程分离的两种实现方式)
-
- [5.10.4.1 方式一:在主线程中分离](#5.10.4.1 方式一:在主线程中分离)
- [5.10.4.2 方式二:线程自我分离](#5.10.4.2 方式二:线程自我分离)
- [5.10.5 线程分离中的时序竞争与同步问题解析](#5.10.5 线程分离中的时序竞争与同步问题解析)
-
- [5.10.5.1 核心矛盾:分离与等待的冲突](#5.10.5.1 核心矛盾:分离与等待的冲突)
- [5.10.5.2 为什么实验中要加 `sleep(1)`?](#5.10.5.2 为什么实验中要加
sleep(1)?) - [5.10.5.3 工业级时序风险:主线程的"生命周期"](#5.10.5.3 工业级时序风险:主线程的“生命周期”)
- [5.10.5.4 总结与最佳实践](#5.10.5.4 总结与最佳实践)
- [5.10.5.5 要点整理](#5.10.5.5 要点整理)
- [5.10.6 Linux 线程库链接机制:从 -lpthread 到 GLIBC 融合](#5.10.6 Linux 线程库链接机制:从 -lpthread 到 GLIBC 融合)
-
- [5.10.6.1 链接期报错:未定义的引用 (Undefined Reference)](#5.10.6.1 链接期报错:未定义的引用 (Undefined Reference))
- [5.10.6.2 解决方案:显式链接线程库](#5.10.6.2 解决方案:显式链接线程库)
- [5.10.6.3 现代系统的变化:Ubuntu 24.04 及更高版本](#5.10.6.3 现代系统的变化:Ubuntu 24.04 及更高版本)
- [5.10.6.4 总结](#5.10.6.4 总结)
- [5.10.7 (小故事收尾)总结:从"监护"到"邻居"](#5.10.7 (小故事收尾)总结:从“监护”到“邻居”)
-
- [5.10.7.1 故事本体](#5.10.7.1 故事本体)
- [5.10.7.2 多进程模型:独立的"分家"行为](#5.10.7.2 多进程模型:独立的“分家”行为)
- [5.10.7.3 多线程模型:共享院落的"邻居"关系](#5.10.7.3 多线程模型:共享院落的“邻居”关系)
- [5.10.7.4 对比总结](#5.10.7.4 对比总结)
- 结尾
4 ~> Linux线程 VS 进程
- 进程间具有独立性
- 线程共享地址空间,也就能够共享进程资源
PCB虽然不一样,但是可以访问同一张文件描述符表。

不同线程的三张表中的 handler表是共享的------共享处理信号的方法,但是pending表和block表是独占的。
4.1 哪些资源是线程独占的?
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分"私有"数据:

每个线程都有自己的线程ID,线程ID先理解成LWP。
最重要的是:
- 线程私有:
组寄存器,线程的上下文数据
切换要对上下文进行保护,所以上下文数据必然是私有的,线程有自己独立的上下文结构。
调函数就要在栈创建栈帧,所以一定要有自己独立的栈结构!每个线程如何拥有独立的栈结构呢?后面会介绍!
- 如果面试的时候把这两点说出来,100分也有85分了

这两个是很重要的,如果面试的话回答这两个是私有的,肯定是非常可以的,面试官会认为这个人对线程是有理解的!对线程要有个动态概念,因为要切换所以是私有线程上下文的,因为要自己执行,所以要有自己的独立的栈结构的。

文件描述符表属于数据结构层面的资源。

我们后续网络会利用共享文件描述符表这个性质。
4.2 进程的多个线程分享

进程和线程的关系:

4.3 关于进程线程的问题
- 如何看待之前学习的单进程?
之前学习的单进程(单执行流)是 具有一个线程执行流的进程。
5 ~> 线程控制
5.1 使用clone,创建LWP
pthread_create底层就是调的clone,Linux下只会给你提供创建轻量级进程的接口,但是我用户想要使用其实是使用的封装好的库。
当然我们之前学过的fork底层也是封装的clone。
Linux操作系统只能提供创建轻量级进程的接口。
- 让AI:帮我生成一段demo代码,使用clone,进行创建LWP。

直接使用clone,创建轻量级进程:

自己不用调,pthread_create底层已经调用封装了clone方法了。

对内核里创建轻量级进程做了封装------用户级线程。
5.2 pthread库
5.2.1 原生线程库
pthread库叫做原生线程库。

只要是Linux系统,就必须带这个库!
5.2.2 pthread的库
- ubuntu24.04中pthread的库

还是能够查到这个库的,只不过在ubuntu24.04下被隐藏了。

老的glibc要带上-l pthread,新的即不需要带-l pthread也能够编译,但是我们还是带上-l pthread。
5.2.2.1 为什么要"明知不需要,却依然带上 -lpthread"的深层原因
这个变化,背后其实是 glibc 架构的一次重大演进,以及对"现代并发"语义的重新定义。

因此,"老的glibc要带上-l pthread,新的即不需要带-l pthread也能够编译,但是我们还是带上-l pthread。"的做法是最佳实践!

5.3 pthread_create接口:几个参数和返回值

5.3.1 参数
(1)第一个参数是一个输出型参数,用户层面的线程ID,这个ID有说法啊(但是这个可不是LWP),我们后面来研究。
(2)第二个基本上都是NULL。
(3)第三个参数是想让你的线程执行那一个方法。
(4)最后一个参数是传递给第三个的,回调。
5.3.2 返回值

5.3.3 思维导图

5.4 进程、线程叫法区分
我们的进程叫父子进程,线程叫主线程和新线程。
5.5 第一件事:验证线程ID
5.5.1 修改代码
把刚才的代码中没用的都注释掉:

现在有了两个线程。

重点关心两个线程的ID值。
无符号长整型:
新线程sleep上一秒,不急着打印。

5.5.2 结论:线程ID的本质其实是一个虚拟地址!
运行一下:

注意!这个 id不是LWP!

ID值并不是LWP,因为线程ID这个东西本来就属于线程库封装的,就不想在暴露轻量级进程里的LWP了,所以不同是正常的,但是这两玩意肯定有关系。
线程ID是pthread库封装的------不想暴露更多细节。
线程ID不一样是正常的,相当于做了隐藏。
Linux用户级线程 :内核LWP = 1 : 1
线程ID不是LWP,那线程ID是什么呢?我们现阶段没办法说。
- 不过我们换种方式打印一下我们可以见一见,一会儿可以直接出个结论------我们以
%p格式打印一下看看(%p打印一下------十六进制,看着顺眼多了。)

多看几个结果:

好了,已经够了,我们可以得出结论了------关于线程ID的本质:
线程ID的本质其实是一个虚拟地址!
线程ID是什么现阶段我们还不关心,后面我们仔细说。
5.5.3 库内部需要对线程做管理吗?
库内部可以存在大量的线程,库内部需要对线程做管理吗?
内部包含了这个结构:

线程库内部一定会存在很多的线程,既然线程库存在了线程概念,那么就必然有些线程刚创建,有的未来会终止等。那库内部需要对线程做管理吗?
线程在库里面要被管理------"先描述,再组织"。
库里面要把线程的属性用结构体描述起来:

线程ID在库里面的源代码:

打印出来的ID值被封装了。
线程块中的起始地址------虚拟地址。
5.5.4 给线程传递任意类型参数
理论上可以给线程传递任意参数(创建新线程的时候,可以传递任意值):

类型:整数、浮点数、字符串......
理论上可以给线程传递任意参数(
void*),整形,浮点型,那我可以传递类或者结构体变量嘛,当然可以。这个void*设计的是很有讲究的,往上整cpp那套。
可以给线程传递类或者结构体变量吗?可以。
- 未来传递任务给线程。
创建一个类:任务。

不要死板地传递void*。

如上图,我们可以传递任务!
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h> // 线程
#include <stdio.h>
#include <string>
// // 全局变量
// int g_val = 100;
// int *p = nullptr; // 指针变量
// void hello(const std::string &name)
// {
// printf("haha,I am common function!,%s\n",name.c_str());
// sleep(5);
// }
class Task
{
public:
Task()
{}
void operator()()
{
// std::cout << "我是任务类..." << std::endl;
}
~Task() {}
private:
};
void *threadrun1(void *args)
{
// p = (int*)malloc(sizeof(int*) * 10);
std::string threadname = static_cast<const char*>(args);
while(true)
{
sleep(1);
std::cout << threadname << std::endl;
// printf("%s isrunning,g_val: %d,&g_val: %p\n",threadname.c_str(),g_val,&g_val);
// sleep(1);
// // hello(threadname);
}
}
void *threadrun2(void *args)
{
std::string threadname = static_cast<const char*>(args);
while(true)
{
sleep(1);
std::cout << threadname << std::endl;
// printf("%s isrunning,g_val: %d,&g_val: %p\n",threadname.c_str(),g_val,&g_val);
// sleep(1);
// g_val++;
// hello(threadname);
// // 故意写异常
// int a = 10;
// a /= 0; // 引发线程异常
}
}
int main()
{
pthread_t t1,t2;
// 我们可以传递给线程类或者结构体变量吗?
Task t;
pthread_create(&t1,nullptr,threadrun1,&t);
pthread_create(&t2,nullptr,threadrun2,(void*)"thread-2");
while(true)
{
printf("Main thread,thread1 id : %p,thread2 id : %p\n",(void*)t1,(void*)t2);
sleep(1);
}
return 0;
}
5.6 POSIX线程库

5.7 操作:创建线程

C语言具有跨平台性,各个平台都对系统调用做了封装。
我们直接来写代码了,这个部分就好理解一些了。
5.7.1 pthread_create
继续来看这个接口:
bash
man 3 pthread_create

跟当年的fork一样。
新线程知不知道自己的id?
5.7.2 pthread_self
- 获取调用pthread自己的id

新线程的ID:

主线程也是线程,当然也可以知道自己的ID:

再打印进程ID:

- 写一个简单的Makefile:
bash
thread:thread.cpp
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f thread
报错了,本来想按16进制打印,这里是十进制:

运行:

5.7.3 创建多线程,给多线程传参
- 主线程把参数交给新线程

我们已经知道"线程可以传递任意参数"。

循环都是主线程在跑。
我怎么知道有多少个线程在跑?

将来每次都执行threadrun接口。
编译:

至此我们一次创建了多线程。
如果我们今天要创建多线程呢,并且让每个线程都有自己的名字。
把name打印出来:

运行:

- 定义缓冲区:构建出线程名
cpp
const int gsize = 64;


为啥会出现这种数字乱了的情况呢?


我们给新线程传递进来的是缓冲区的起始地址,可能会导致还没有拷贝到里面呢,就因为主线程下次循环给他改掉了,可能会导致第一个线程的名字被设置成第二个了。
谁先被调度还不一定,刚刚主要是因为主线程跑的快了,给下一个线程创建做准备了,对缓冲区进行修改了,虽然是在栈空间,但是每次都是它还没有变,老的线程拿到了新的名字。
根本原因 是:threadname是共享资源,主线程的栈空间内容是可以被其它的线程拿到的,同理其它线程的局部变量,如果主线程知道地址,也是可以拿到的。
我们给新进程传进去的其实是缓冲区的起始地址,可能会导致还没拷贝到threadrun函数里的name里面呢,就因为主线程下次循环给他改掉了,可能会导致第一个线程的名字设置成第二个了。
谁先调度完全不确定,刚刚主要是因为主线程跑的快了,给下一个线程创建做准备了,对缓冲区进行修改了,
虽然是在栈空间但是每次都是它没有变换,可能就会修改它了,那里面*args依旧是指向这个缓存区的地址的,所以就会出问题。
正常情况下我们是看不到其它线程的局部变量的,但是我们在全局定义指向这个地址,那就有可能看到了。
根本原因:threadname这个缓存区是每个线程都共享的,主线程的栈空间是可能被其他的线程拿到的,同理其他线程的局部变量,如果主线程知道地址也可以拿到。正常我们是看不其他线程的局部变量的,互相看不到。但是我们在全局定义指向这个地址,那就可能能看到了。
好,回到这个缓冲区,我们这个缓冲区是公共资源的,所以这个是具有线程安全问题的,这里的问题是线程不一致。
为啥加个sleep可以勉强解决,因为我们让他停了会,但是这个不是真正的解决方法。


证明是在主线程跑的:

怎么解决?我们new一下:

你的意思是,有多少个线程,就要new多少个空间?哎,我就是这个意思。
十个线程,十个空间,这样每个线程拿到的参数的地址就是不一样的了。


5.7.4 构建任务:可以传递C++中的类对象
- 可以给线程派发任务
我们可以构建一个任务:

来一个仿函数:

- 构建一个无参数版本的

任务将来由谁来完成:

主线程创建了任务:


运行:

数据存在黏合:

让新线程不要出现重合:

运行,依次完成任务:

至此我们把pthread_create了解完了:
给线程可以传递的参数可以很多!无非就是增加类内的成员属性------格局打开。
- 下面我通过合理的内存分配(Heap)和对象封装,从架构上彻底规避多线程传参的竞态风险。
5.7.4.1 核心逻辑:把线程"装进"对象里
以前写多线程像开野车,直接调 pthread_create,参数乱飞。现在你图里的做法是做了一个 "自动驾驶座舱" (类 Thread)。我们只需要定义好这个座舱的功能,点一下"启动",它自己就去联系内核跑起来了。
5.7.4.2 知识点代码拆解:核心实现
整个逻辑分为三个层次。
第一层:任务数据的封装(结构体)
cpp
// 对应图中 ThreadData 逻辑
struct ThreadData {
pthread_t tid; // 线程 ID
char name[64]; // 线程的名字(通过 snprintf 写入)
// ... 其他任务相关数据
};
第二层:线程的抽象(类封装)
图中的 class Thread 将 Linux 原生的 C 接口封装成了 C++ 对象:
-
私有成员:存储线程 ID、状态和数据。
-
回调函数
Routine:作为pthread_create的第三个参数(必须是静态或全局函数,因为 C 接口不识别this指针)。
第三层:主逻辑的循环创建
cpp
// 对应图中 main 函数及运行过程
for (int i = 0; i < thread_num; i++) {
// 关键点:这里为每个线程"量身定制"了一个 ThreadData 对象
ThreadData *td = new ThreadData();
snprintf(td->name, sizeof(td->name), "thread-%d", i);
// 调用封装好的接口启动线程
pthread_create(&td->tid, NULL, threadrun, (void*)td);
}
5.7.4.3 运行过程与底层原理
(1)对象的生命周期
主线程在循环中不断 new 出新的 ThreadData 空间。这些空间位于 堆(Heap) 上,是互相独立、物理隔离的。
(2)地址传递的"安全性"
虽然主线程快速跑完了循环,但传给每个子线程的地址是不同的。
-
线程 1 拿到的是地址
0x100,内容是 "thread-0"。 -
线程 2 拿到的是地址
0x200,内容是 "thread-1"。
主线程修改 0x200 时,完全不会影响到线程 1 正在读取的 0x100。这就是为什么你运行结果里线程名能一一对应、不混乱。
(3)回调与上下文切换
当 pthread_create 返回后,内核调度子线程运行。子线程通过传进来的 void* 指针,重新强转回ThreadData*,从而拿到了属于自己的所有上下文。
5.7.4.4 总结:逻辑严密性检查
1、为什么代码要这么写?
- 为了解耦。主线程只管发指令,子线程通过结构体拿数据,互不干扰。
2、图中强调的"公共资源"在哪里?
- 虽然每个线程的数据是独立的,但它们共享同一个打印屏幕(终端)。如果你看运行截图,输出的行序可能会交错,这就是对公共资源(标准输出)的抢占,但数据本身(线程名)是正确的。
3、费曼式反思:
-
如果不用
new而是用局部变量,就是我上个回答里说的"白板"问题; -
用了
new,就是给每个线程发了"独立名牌"。
5.7.5 使用PS命令查看线程信息
运行代码后执行:
bash
$ ps -aL | head -1 && ps -aL | grep mythread
PID LWP TTY TIME CMD
2711838 2711838 pts/235 00:00:00 mythread
2711838 2711839 pts/235 00:00:00 mythread
-L 选项:打印线程信息
LWP是什么呢?我们已经知道:LWP得到的是真正的线程ID。之前使用pthread_self得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
在ps -aL得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的,而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
5.8 线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:

5.8.1 pthread_exit函数
功能:线程终止。
原型:
cpp
void pthread_exit(void *value_ptr);
-
参数:
value_ptr,value_ptr不要指向⼀个局部变量。 -
返回值:⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
5.8.2 pthread_cancel函数
功能:取消一个执行中的线程。
原型:
cpp
int pthread_cancel(pthread_t thread);
-
参数:
thread,线程ID。 -
返回值:成功返回0;失败返回错误码
5.9 线程等待
为什么需要线程等待?
-
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
-
创建新的线程不会复用刚才退出线程的地址空间。
cpp
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过
pthread_join得到的终止状态是不同的,总结如下:
pthread_join获取线程退出状态

5.9.1 技术板块:pthread_join 线程退出状态回收
(1)功能概述
调用 pthread_join 的线程会挂起等待,直到目标线程终止。它类似于进程中的 waitpid,主要负责 "收尸" (回收资源)和 "取信" (获取退出状态)。
(2)四种终止场景与结果

5.9.2 深度逻辑:为什么参数是二级指针 void **?
这是很多人的理解难点,用费曼法一句话点破:
本质需求:子线程返回的是一个地址(指针),主线程想用自己本地的一个变量(也是指针)去接收它。
传参原理:在 C 语言中,如果你想在函数内部修改外部变量的值,必须传递该变量的地址。
推导结论:
-
修改
int,传int*。 -
修改
void*(指针),自然要传void**(指针的地址)。
5.9.3 代码实现参考
cpp
void *retval = NULL; // 准备一个信箱
pthread_join(tid, &retval); // 传信箱的地址过去
if (retval == PTHREAD_CANCELED) {
printf("线程被取消了\n");
} else {
printf("线程正常退出,返回值为: %ld\n", (long)retval);
}
逻辑总结:
pthread_join 是一个典型的同步等待机制。它不仅确保了主子线程的时序安全(主等子),还通过"地址传参"的方式,精准地把子线程在临死前最后一块内存里的数据"掏"了出来。
5.9.4 样例代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread1(void *arg)
{
printf("thread 1 returning ... \n");
int *p = (int *)malloc(sizeof(int));
*p = 1;
return (void *)p;
}
void *thread2(void *arg)
{
printf("thread 2 exiting ...\n");
int *p = (int *)malloc(sizeof(int));
*p = 2;
pthread_exit((void *)p);
}
void *thread3(void *arg)
{
while (1)
{ //
printf("thread 3 is running ...\n");
sleep(1);
}
return NULL;
}
int main(void)
{
pthread_t tid;
void *ret;
// thread 1 return
pthread_create(&tid, NULL, thread1, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
free(ret);
// thread 2 exit
pthread_create(&tid, NULL, thread2, NULL);
pthread_join(tid, &ret);
printf("thread return, thread id %X, return code:%d\n", tid, *(int *)ret);
free(ret);
// thread 3 cancel by other
pthread_create(&tid, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n",
tid);
else
printf("thread return, thread id %X, return code:NULL\n", tid);
}
运行结果:
bash
[root@localhost linux]# ./a.out
thread 1 returning ...
thread return, thread id 5AA79700, return code:1
thread 2 exiting ...
thread return, thread id 5AA79700, return code:2
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread return, thread id 5AA79700, return code:PTHREAD_CANCELED

5.10 线程分离
我们把线程创建出来,默认该线程必须被join的(joinable)!如果不想等,也不关心线程的退出情况------可以把目标线程设置为分离状态!(一旦线程被设置为分离状态,就不需要关心线程的退出情况,不需要等待了,主线程是最后退出的)
如果我们未来不想join阻塞等待新线程呢? 压根就不想等待呢!
比如我们之前的多进程就可以不用等待了,忽略掉
SIGCHLD,但是我们线程是没非阻塞等待的。
我们可以把线程设置成分离,如果进行线程分离,主线程和新线程就各自跑各自的了。
5.10.1 pthread_detach
在 Linux 中,线程默认是 joinable 状态。这意味着线程退出后,其资源不会被立即释放,直到主线程调用 pthread_join。为了实现资源的自动回收,需要将线程设置为 detached 状态。
线程分离的接口:
- 函数原型:

-
功能 : 将指定的线程标记为分离状态。当分离的线程终止时,其资源会自动释放回系统,无需其他线程对其进行
join操作。 -
注意 : 对已经处于分离状态的线程再次调用
detach会导致未定义行为。
cpp
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
cpp
pthread_detach(pthread_self());
该如何分离线程呢?

5.10.2 joinable

返回值为0,正常的、没有分离的;设置了分离,返回值肯定不是0了。

改一下:

运行一下:

- 实验结论与错误排查

5.10.3 操作(主要留意上面的实验,这个看过就行)
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg )
{
pthread_detach(pthread_self());
printf("%s\n", (char*)arg);
return NULL;
}
int main( void )
{
pthread_t tid;
if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 )
{
printf("create thread error\n");
return 1;
}
int ret = 0;
sleep(1);//很重要,要让线程先分离,再等待
if ( pthread_join(tid, NULL ) == 0 )
{
printf("pthread wait success\n");
ret = 0;
}
else
{
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
5.10.4 线程分离的两种实现方式
5.10.4.1 方式一:在主线程中分离
在创建线程后,直接在主线程中使用子线程的 tid 进行分离。
cpp
pthread_t tid;
pthread_create(&tid, nullptr, threadrun, (void*)"Thread-1");
pthread_detach(tid); // 主线程主动分离子线程
5.10.4.2 方式二:线程自我分离
子线程在执行函数内部通过 pthread_self() 获取自身 ID 并完成分离。
cpp
void* threadrun(void* args) {
pthread_detach(pthread_self()); // 子线程自我分离
// 业务逻辑...
return nullptr;
}
介绍线程ID的时候会说:什么是线程的返回值、线程的属性有哪些?

线程被分离:

5.10.5 线程分离中的时序竞争与同步问题解析
在多线程编程中,时序问题 (Race Condition / Timing Issue) 是最隐蔽的 Bug 来源。在你提供的实验代码中,这种时序竞争主要体现在"分离状态生效"与"主线程执行逻辑"之间的博弈。

5.10.5.1 核心矛盾:分离与等待的冲突

5.10.5.2 为什么实验中要加 sleep(1)?

5.10.5.3 工业级时序风险:主线程的"生命周期"

5.10.5.4 总结与最佳实践

5.10.5.5 要点整理
(1)pthread_join 在实验中是用来判断分离是否成功的"检测器"。
(2)sleep(1) 是为了让子线程在竞争中"跑赢"主线程,确保分离状态先被设置。
(3)必须警惕主线程退出导致分离子线程被动"夭折"的工业事故。
5.10.6 Linux 线程库链接机制:从 -lpthread 到 GLIBC 融合
5.10.6.1 链接期报错:未定义的引用 (Undefined Reference)

5.10.6.2 解决方案:显式链接线程库

5.10.6.3 现代系统的变化:Ubuntu 24.04 及更高版本

5.10.6.4 总结

5.10.7 (小故事收尾)总结:从"监护"到"邻居"
线程分离的本质是管理权的移交:
-
(1)默认状态 (Joinable): 类似于父子关系,主线程负有监护责任,必须负责最后的资源回收。 -
(2)分离状态 (Detached): 类似于邻居关系,大家共享院落(地址空间),但各自独立生活,互不干涉生老病死(资源由系统自动回收)。
本质是对计算机资源分配与线程分离的类比分析。
5.10.7.1 故事本体
我们之前讲过社会资源分配的基本单位(实体)是家庭,以前年代家庭成员可能会比较多,出现分家这种情况,分家可以是父亲跟儿子说也可以儿子跟父亲说,主线程分离新线程就是父亲跟儿子说,儿子跟父亲说就是自己分离------这个故事更适用于多进程,自己有自己独立的资源、虚拟地址空间等。线程的话其实还是共享了一些资源的,只是互不关心而已,就像一个院子里房子单独分隔一间给你,从家人变成了邻居。
5.10.7.2 多进程模型:独立的"分家"行为

5.10.7.3 多线程模型:共享院落的"邻居"关系

5.10.7.4 对比总结

结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

