【Linux线程】Linux系统多线程(三):Linux线程 VS 进程,线程控制

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《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.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_ptrvalue_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有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【Linux线程】Linux系统多线程(二):线程的优缺点

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
浮芷.2 小时前
Flutter 框架跨平台鸿蒙开发 - 智能家电故障诊断应用
运维·服务器·科技·flutter·华为·harmonyos·鸿蒙
徒 花2 小时前
Python知识学习07
windows·python·学习
栈低来信2 小时前
VFS虚拟文件系统
linux
jekc8682 小时前
Ubuntu-GitLab
服务器·ubuntu·gitlab
浮芷.2 小时前
Flutter 框架跨平台鸿蒙开发 - 急救指南应用
学习·flutter·华为·harmonyos·鸿蒙
小白天下第一2 小时前
java+三角测量(两个工业级)+人体3d骨骼关键点获取(yolov8+HRNET_w48_2d)
java·yolo·3d·三角测量
x-cmd2 小时前
macOS 内存模型深度解析 | x free 设计哲学
linux·macos·内存·策略模式·free·x-cmd
航Hang*2 小时前
网络安全技术基础——第3章:网络攻击技术
运维·网络·笔记·安全·web安全·php
独小乐2 小时前
007.GNU C内联汇编杂谈|千篇笔记实现嵌入式全栈/裸机篇
linux·c语言·汇编·单片机·嵌入式硬件·arm·gnu