线程的控制

这里写目录标题

  • [🧵 深入理解线程:资源模型、切换机制与并发策略](#🧵 深入理解线程:资源模型、切换机制与并发策略)
    • [1. 线程的本质:轻量级进程 (LWP)](#1. 线程的本质:轻量级进程 (LWP))
    • [2. 性能核心:为什么线程切换更快?](#2. 性能核心:为什么线程切换更快?)
      • [⚡ 线程切换 vs 进程切换](#⚡ 线程切换 vs 进程切换)
      • [💡 关于 Cache 的补充认知](#💡 关于 Cache 的补充认知)
    • [3. 应用场景:IO 密集型 vs 计算密集型](#3. 应用场景:IO 密集型 vs 计算密集型)
    • [4. 线程的代价与风险](#4. 线程的代价与风险)
    • [5. 并发安全与内存陷阱](#5. 并发安全与内存陷阱)
    • [6. 线程创建核心:`pthread_create` 参数详解](#6. 线程创建核心:pthread_create 参数详解)
      • [🛠️ 函数原型](#🛠️ 函数原型)
      • [🔍 参数深度解析](#🔍 参数深度解析)
      • [4. `arg` (传递给线程的参数) ------ ⭐ 万能接口与类型转换](#4. arg (传递给线程的参数) —— ⭐ 万能接口与类型转换)
        • [💻 C 语言写法 (注重强制转换)](#💻 C 语言写法 (注重强制转换))
        • [💻 C++ 写法 (注重类型安全与封装)](#💻 C++ 写法 (注重类型安全与封装))
        • [💡 关于传递"函数指针"的特别说明](#💡 关于传递“函数指针”的特别说明)
    • [7. ⚠️ 致命弱点:一损俱损 (Crash Propagation)](#7. ⚠️ 致命弱点:一损俱损 (Crash Propagation))
    • [8. 并发陷阱:数据竞争与"覆盖"真相(线程并发问题)](#8. 并发陷阱:数据竞争与“覆盖”真相(线程并发问题))
      • [✅ 真正的元凶:共享资源的非原子访问](#✅ 真正的元凶:共享资源的非原子访问)
      • [🛡️ 解决方案](#🛡️ 解决方案)
    • [9. 线程与进程的退出机制](#9. 线程与进程的退出机制)
      • [1. `exit()` (进程级退出) ------ ⚠️ 危险操作](#1. exit() (进程级退出) —— ⚠️ 危险操作)
      • [2. `return` (线程级退出)](#2. return (线程级退出))
      • [3. `pthread_exit` (线程主动退出)](#3. pthread_exit (线程主动退出))
      • [4. `pthread_cancel` (外部强制取消,成功0,失败错误码,不建议)](#4. pthread_cancel (外部强制取消,成功0,失败错误码,不建议))
      • [10. 线程等待 (pthread_join) 详解](#10. 线程等待 (pthread_join) 详解)
      • [11. void\* 与 void\*\* 的类型安全与转换规则 (重点)](#11. void* 与 void** 的类型安全与转换规则 (重点))
      • [12. 线程分离 (pthread_detach)](#12. 线程分离 (pthread_detach))
      • [13. 线程与进程/程序替换](#13. 线程与进程/程序替换)
      • [14. C++ 多线程与 pthread 的对应关系](#14. C++ 多线程与 pthread 的对应关系)
      • [15. 底层内存模型与实现原理 (核心)](#15. 底层内存模型与实现原理 (核心))

🧵 深入理解线程:资源模型、切换机制与并发策略

1. 线程的本质:轻量级进程 (LWP)

在 Linux 内核视角下,线程被称为轻量级进程。虽然 CPU 调度的基本单位依然是 PCB(进程控制块),但相比传统进程,线程实现了极致的轻量化。

核心特征

  • 资源共享(进程级): 同一进程内的所有线程共享虚拟内存空间(代码区、数据区、堆区、文件描述符等)。
  • 私有资源(线程级): 每个线程拥有独立的执行上下文,主要包括:
    • 寄存器值: 保存当前的运算状态,确保中断恢复后能继续执行。
    • 线程栈: 独立存储局部变量、函数调用链(压栈信息)。
    • 线程 ID (TID)。

💡 关键点: 资源是由进程统一申请的,线程只是依附于进程存在的执行流。"皮之不存,毛将焉附"------一旦进程崩溃(PID 被杀),其下的所有线程也会随之消亡。一个线程的异常崩溃往往会导致整个进程挂掉。


2. 性能核心:为什么线程切换更快?

创建和销毁线程的代价远小于进程,其核心优势在于上下文切换时的 Cache 友好性

⚡ 线程切换 vs 进程切换

  1. 虚拟地址空间不变: 同进程内的线程切换不需要切换页表(Page Table),TLB(Translation Lookaside Buffer)无需刷新。
  2. Cache 命中率高(关键):
    • 线程切换: 因为共享地址空间,CPU Cache 中的数据对下一个线程依然有效,不需要清空 Cache
    • 进程切换: 由于地址空间改变,原有的 Cache 内容往往失效。系统通常会丢弃旧 Cache,导致新进程调度上来时经历"冷启动",需要重新从内存加载数据("冷 -> 热"过程),这非常耗时。
  3. 寄存器开销小: 仅需保存和恢复少量通用寄存器。

💡 关于 Cache 的补充认知

  • 用户级缓冲区: 减少用户态程序与内核态的交互次数(如 stdio 库缓冲)。
  • 内核级缓冲区: 减少磁盘 IO 与内核的交互次数(如 Page Cache)。
  • CPU Cache (L1/L2/L3): 减少 CPU 与内存(RAM)的交互次数。
    • 注意: Cache 是纯硬件实现的,由 CPU 内部控制器管理,操作系统内核无法直接干预。它只认物理/虚拟地址,不管你是内核代码还是用户代码。

3. 应用场景:IO 密集型 vs 计算密集型

根据任务特性选择合适的并发模型至关重要。

类型 特征 典型场景 多线程策略
IO 密集型 CPU 经常处于空闲等待状态(等磁盘、等网络)。 文件读写、网络爬虫、Web 服务、数据库查询。 推荐多线程。 利用线程等待 IO 的时间片去执行其他线程,实现 IO 操作重叠,提高吞吐量。
计算密集型 CPU 满载运行,一直在进行数学运算或逻辑判断。 视频解码、图像渲染、矩阵乘法、复杂算法。 多核环境推荐多线程。 将计算分解到多个核上并行处理。单核环境不推荐。 线程切换反而会带来额外的上下文开销,不如单线程。

经验法则: 对于计算密集型任务,通常创建的线程数与 CPU 核心数一一对应效果最佳。


4. 线程的代价与风险

虽然线程高效,但也引入了复杂性:

  1. 健壮性降低: 缺乏访问控制隔离。一个线程非法访问内存导致崩溃(Segmentation Fault),会导致整个进程被操作系统杀掉,其他无辜线程也随之终止。
  2. 编程难度提升: 必须处理复杂的并发问题。
  3. 单核性能损耗: 在单核 CPU 上进行计算密集型任务的多线程切换,纯属浪费资源。

5. 并发安全与内存陷阱

栈的私有性与指针越界

  • 默认隔离: 多线程中,栈资源是不共享的。线程 A 不知道线程 B 的栈在哪里,互不干扰。
  • 指针破坏隔离: 如果你将一个线程的栈变量指针 传递给另一个线程,或者通过全局变量传递指针,就可以打破这种隔离,访问对方的栈空间。这非常危险,因为当原线程函数返回后,栈帧销毁,该指针就会变成悬空指针(Dangling Pointer)

并发冲突解决

  • 问题: 如果多个线程同时操作同一个数组或共享资源,会发生数据覆盖(Race Condition)。
  • 对策:
    1. 加锁: 使用互斥锁(Mutex)保护共享资源。
    2. 私有化: 尽量不给共享数组,而是为每个线程 new/malloc 开辟独立的堆内存,保证地址不同,从物理上隔离数据。

📝 总结

一边看电影(IO密集)一边下载资源(IO密集),底层就是典型的多线程应用。理解线程的关键在于把握**"共享地址空间"**带来的便利(切换快、通信易)与风险(一崩全崩、并发冲突)。


关于传参问题

6. 线程创建核心:pthread_create 参数详解

在 Linux (POSIX) 环境下,我们使用 pthread_create 来启动一个新线程。理解它的四个参数是掌握多线程编程的第一步。

🛠️ 函数原型

c 复制代码
#include <pthread.h>

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

🔍 参数深度解析

  1. thread (线程 ID)

    (存在问题:1.如果你创建的是一个tid变量,其他线程id会被覆盖,主线程无法获取全部,所以要用一个vector储存)

    • 类型: pthread_t *
    • 作用: 这是一个输出型参数。调用成功后,系统会把新线程的唯一标识符(ID)写入这个指针指向的变量中。
    • 意义: 它是主线程管理子线程的"身份证",后续如果想等待线程结束 (pthread_join) 或分离线程,都需要用到它。
  2. attr (线程属性)

    • 类型: const pthread_attr_t *
    • 作用: 用于配置线程的高级属性(如栈大小、调度优先级等)。
    • 实战建议: 99% 的场景下,直接传 NULL 即可,表示使用系统默认属性。
  3. start_routine (回调函数/入口点)

    • 类型: void *(*start_routine) (void *)
    • 作用: 这是一个函数指针。新线程一旦启动,就会立即跳转到这个函数开始执行代码。
    • 约束: 你的回调函数必须严格遵守"参数是 void*,返回值也是 void*"的格式。

4. arg (传递给线程的参数) ------ ⭐ 万能接口与类型转换

  • 本质: void* 是"通用指针"。它不代表数据本身是空的,而是代表**"我只负责搬运这个地址,不关心具体类型"**。
  • 你可以传什么?
    • 基本变量地址: int, float 等的地址。
    • 结构体/类对象: 最常用!把复杂数据打包成一个对象,传它的地址。
    • 函数指针: 没错,函数名本身就是地址。你可以把回调函数的地址强转传进去,实现灵活的逻辑调用。
💻 C 语言写法 (注重强制转换)

在 C 语言中,void* 可以隐式转换为其他指针,但为了代码清晰和安全,建议显式转换。

c 复制代码
// 1. 定义一个数据结构
typedef struct {
    int id;
    char name[20];
} TaskData;

// 2. 定义一个回调函数(作为参数传递)
void print_hello() {
    printf("Hello from callback!\n");
}

// 3. 线程执行函数
void* worker(void* arg) {
    // 【关键】将 void* 强转回原来的结构体指针
    TaskData* data = (TaskData*)arg; 
    
    printf("ID: %d, Name: %s\n", data->id, data->name);
    
    // 如果传的是函数指针,这里也要转回来调用
    // void (*func_ptr)() = (void (*)())data->callback; 
    // func_ptr();
    
    return NULL;
}

// 4. 主函数调用
int main() {
    TaskData my_task = {101, "Worker A"};
    pthread_t tid;

    // 传入结构体地址,(void*) 强转是为了匹配参数类型
    pthread_create(&tid, NULL, worker, (void*)&my_task);
    
    pthread_join(tid, NULL);
    return 0;
}
💻 C++ 写法 (注重类型安全与封装)

C++ 编译器禁止 void* 隐式转换为其他指针,必须使用 static_cast 或 C 风格强转。同时,C++ 常利用类成员函数(需静态化)或 Lambda(需特殊处理)来配合。

cpp 复制代码
#include <iostream>
#include <pthread.h>

struct TaskData {
    int id;
    std::string name;
};

// 注意:pthread_create 只能接受普通函数或静态成员函数
// 不能直接传非静态成员函数,因为缺少 this 指针
class Worker {
public:
    static void* run(void* arg) {
        // 【关键】C++ 中使用 static_cast 进行安全的向下转型
        TaskData* data = static_cast<TaskData*>(arg);
        
        std::cout << "C++ Thread -> ID: " << data->id 
                  << ", Name: " << data->name << std::endl;
        return nullptr;
    }
};

int main() {
    TaskData task{202, "CPP Worker"};
    pthread_t tid;

    // C++ 中也可以省略 (void*),因为 void* 可以接收任意指针
    // 但为了语义明确,保留也不为过
    pthread_create(&tid, nullptr, Worker::run, &task);
    
    pthread_join(tid, nullptr);
    return 0;
}
💡 关于传递"函数指针"的特别说明

如果你想通过第四个参数传递一个函数让线程去执行(类似策略模式):

  1. 打包传递: 最好把函数指针和它需要的数据一起打包进一个 struct,然后传这个 struct 的地址。
  2. 单独传递: 如果只传一个函数指针:
    • C: pthread_create(..., thread_func, (void*)my_callback_func);
    • 线程内恢复: void (*func)() = (void (*)())arg; func();
    • C++: 同样需要 reinterpret_cast<void*>(my_callback_func) 才能塞进 void*,取回时再转回去。

⚠️ 避坑指南:

无论 C 还是 C++,如果你传递的是局部变量的地址(如上面的 &my_task),必须确保该变量在线程结束前不被销毁 。如果主线程跑得太快,局部变量出了作用域被回收,子线程拿着这个地址去访问就会发生段错误 (Segmentation Fault) ,导致整个进程崩溃(正如我们第 7 点提到的)。对于短生命周期变量,建议使用 new/malloc 分配堆内存,并在子线程中释放。


7. ⚠️ 致命弱点:一损俱损 (Crash Propagation)

正如你所总结的:线程虽然执行流独立,但命运是绑定在一起的。

  • 现象: 如果主线程(Main Thread)崩溃,或者任意一个子线程发生了未捕获的严重错误(如段错误 Segmentation Fault、非法内存访问),整个进程会立即终止
  • 后果: 进程内所有的其他线程,无论它们当时正在做什么(哪怕是在正常处理业务),都会被迫瞬间停止。

8. 并发陷阱:数据竞争与"覆盖"真相(线程并发问题)

** 每个线程都有自己独立的栈空间(私有资源)。线程 A 的局部变量和线程 B 的局部变量在内存地址上是完全隔离的,互不干扰。

✅ 真正的元凶:共享资源的非原子访问

所谓的"覆盖"或数据错乱,通常发生在以下场景:

  1. 传递了同一个指针:
    当你调用 pthread_create(..., arg) 时,如果多个线程接收到的 arg 指向的是同一块堆内存全局变量
  2. 同时读写(Race Condition):
    • 线程 A 读到数据是 10,准备加 1。
    • 此时 CPU 切换到线程 B,它也读到数据是 10,也加 1 并写回(变成 11)。
    • CPU 切回线程 A,它基于之前的旧值 10 计算,写回 11。
    • 结果: 加了两次,结果却是 11(期望是 12)。这就叫"丢失更新"或"覆盖"。

🛡️ 解决方案

为了解决这个问题,我们必须引入同步机制

  • 互斥锁: 保证同一时刻只有一个线程能操作这块共享数据(就像上厕所锁门一样)。

9. 线程与进程的退出机制

1. exit() (进程级退出) ------ ⚠️ 危险操作

  • 作用: 终止整个进程
  • 后果: 进程内的所有线程(无论正在做什么)都会被强制杀掉,进程资源被回收。
  • 注意: 绝对不要在子线程里调用 exit(),除非你想让整个程序同归于尽。

2. return (线程级退出)

  • 作用: 在线程入口函数中直接 return,仅结束当前线程。
  • 返回值: 返回一个 void* 类型的值(用于传递给 pthread_join)。
  • 适用场景: 线程正常执行完毕。
  • ⚠️ 禁忌: 主线程千万不要用 return! 主线程 return 等同于调用 exit(),会导致整个进程崩溃。

3. pthread_exit (线程主动退出)

  • 作用: 在线程内部调用,强制提前终止当前线程。
  • 参数: void *retval
    • C 语言写法: 通常传 NULL
    • C++ 写法: 推荐传 nullptr
    • 作用: 向调用 pthread_join 等待它的线程传递返回值。如果不需要返回值,直接传 nullptr 即可。

4. pthread_cancel (外部强制取消,成功0,失败错误码,不建议)

  • 作用:其他线程(通常是主线程)调用,向目标线程发送"取消请求"。
  • 参数: pthread_t thread(目标线程的 ID)。
  • ⚠️ 核心注意:
    • 不是立即杀死线程,只是发个通知。
    • 目标线程必须运行到"取消点"(如 sleep, read, pthread_join 等阻塞函数)才会真正退出。

10. 线程等待 (pthread_join) 详解

  • 核心作用:

    1. 防止资源泄漏(必须): 类似进程中的 wait 防止僵尸进程。新线程退出后,若不被 join,其内核 PCB (task_struct) 虽释放,但用户态动态库中的描述结构体 (struct pthread) 仍占用内存,导致"线程级"内存泄漏。
    2. 获取返回值(可选): 主线程阻塞等待,直到目标线程结束。
  • 函数原型:

    c 复制代码
    int pthread_join(pthread_t thread, void **retval);
    • 参数1 (thread): 目标线程 ID。
    • 参数2 (retval): 输出型参数 ,类型为 void**。用于接收线程函数的返回值。若不关心返回值可传 NULL
    • 返回值: 成功返回 0,失败返回错误码。
  • 返回值的三种情况:

    1. 正常 return: retval 指向的内容即为 return 的值。
    2. 调用 pthread_exit retval 指向的内容为传给 pthread_exit 的参数。
    3. 被取消 (pthread_cancel): retval 指向的内容为常数 PTHREAD_CANCELED

11. void* 与 void** 的类型安全与转换规则 (重点)

  • void* (万能指针):
    • 特性: 可以接收任意类型的指针(如 int*, MyClass*void*),因为所有指针在内存中存储的都是地址,大小相同。
    • 限制: C++ 规定,当参数是 void* 时,不允许对该变量解引用去修改对应的值(编译器不知道读多少字节)。
  • void** (二级指针):
    • 作用: 允许函数通过解引用修改外部的 void* 变量本身(即修改它指向哪里)。
    • 严禁操作: 绝对禁止T* 的地址强转为 void** 传入。
      • 原因: 虽然 void* 大小固定,但 T* 指向的对象大小未知。如果允许这种转换,函数内部尝试写入数据时,编译器无法确定应该写入多少个字节,也无法保证对齐方式,会导致未定义行为。
    • 正确做法: 必须先定义一个标准的 void* 变量作为中转站,获取返回值后再强转回业务类型。

12. 线程分离 (pthread_detach)

  • 背景: 默认创建的线程是 joinable 的。如果不关心返回值且不想阻塞等待,可以将线程设置为分离状态
  • 特点: 线程退出后自动释放资源(包括内核栈和 struct pthread),无需主线程 join。
  • 接口: int pthread_detach(pthread_t thread);
  • 注意:
    • 一旦分离,无法再 join
    • 主线程不能提前退出(否则进程结束,子线程也会消亡)。
    • 分离线程发生异常(如除0)仍会导致整个进程崩溃,因为它们共享地址空间。

13. 线程与进程/程序替换

  • 线程内 fork: 相当于当前进程调用 fork。
  • 线程内 exec (程序替换): 极度不推荐。因为线程共享页表,替换会破坏其他线程的运行环境,通常报错。若需替换,建议先 fork 子进程,在子进程中 exec。

14. C++ 多线程与 pthread 的对应关系

  • 本质: Linux 下 C++ 的多线程本质上是对 pthread 库的封装。
  • 参数传递差异:
    • pthread_create:第四个参数可以是类对象指针(强转 void*),因为只是读取数据。
    • pthread_join:第二个参数必须是 void* 变量的地址(即 void**),因为要写入返回值。

15. 底层内存模型与实现原理 (核心)

  • 内存布局:
    • 主线程: 运行在进程的主栈中。
    • 新线程: 并不在内核空间直接运行,而是在用户态的动态库(mmap区域)中拥有自己的 struct pthread 结构和独立的线程栈
  • tid 的本质:
    • pthread_t tid 实际上就是动态库中 struct pthread 结构体的起始地址
    • Join 的寻址过程: 当调用 pthread_join(tid, ...) 时,系统将传入的 tid(地址)强转为 struct pthread* 指针,直接访问该结构体内部的成员(如 result 变量)来获取返回值或检查状态。
  • 1:1 模型: struct pthread (用户态) 与 task_struct (内核态) 是一一对应的。
  • 创建流程: pthread_create -> 初始化 pthread_attr -> 调用底层 clone 系统调用 -> 返回 tid (即结构体地址)。
  • 线程局部存储 (TLS): 每个线程在 struct pthread 旁有独立的 TLS 区域,用于存储如 errno、线程 ID 等独立变量,互不干扰。

相关推荐
写代码的学渣2 小时前
Linux 彻底清空历史命令 & 恢复历史命令(完整版、重启永久生效)
linux·运维·chrome
生万千欢喜心2 小时前
liunx AnolisOS-8.9-x86_64-dvd.iso win11 适配龙蜥 - 虚拟机
linux
天空'之城3 小时前
Linux 系统编程 12:TCP 进阶
linux·网络编程
红糖奶茶4 小时前
怎么样在遍历列表时动态判断阈值是否被满足并返回相应文本
java·linux·服务器
bkspiderx4 小时前
rpath:Linux 中固定共享库搜索路径的“永久解决方案”
linux·rpath·固定共享库搜索路径
wbs_scy4 小时前
仿 muduo 高并发服务器项目:从 timerfd 到时间轮实现定时任务机制
linux·服务器·c++
AI的探索之旅4 小时前
从 Ubuntu 14.04 到 24.04:TI AM335x 开发环境完整迁移与 Agent 接管方案
linux·数据库·嵌入式硬件·ubuntu·postgresql
阿pin4 小时前
Android随笔-pipe是什么?
android·linux·pipe
小趴菜え4 小时前
升级Ubuntu20.04版本至Ubuntu22.04
linux·运维