线程的控制

这里写目录标题

  • [🧵 深入理解线程:资源模型、切换机制与并发策略](#🧵 深入理解线程:资源模型、切换机制与并发策略)
    • [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 等独立变量,互不干扰。

相关推荐
承渊政道5 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.8 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落10 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨10 小时前
WSL2的安装与使用
linux·wsl
微小的鱼10 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng10 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮11 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky1 天前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默1 天前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk81631 天前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd