一、线程概念
1.1 理解线程
通俗理解:线程是进程中的一个执行分支 (执行流),CPU 实际在跑的那条指令序列
本质理解 :Linux 内核中进程和线程使用同一个数据结构 task_struct 来表示,所以线程本质上就是共享部分资源的"轻量级进程"
打个比方:
进程像一家工厂,拥有厂房、设备、原料 (内存、文件等资源)
线程像工厂里的工人,每个工人各自干活,共享工厂的一切资源
工厂至少要有一个工人,所以一个进程至少有一个线程 (主线程)
多招几个工人(线程),他们共用厂房原料,沟通成本极低,但一个工人操作失误弄坏原料(写坏共享内存),全厂遭殃。
1.2 进程与线程共享的资源
从内核和资源的角度:在Linux中,进程是资源分配的单位(负责管理内存和文件),线程是调度的单位(负责占用CPU)。
"线程是轻量级进程"------站在Linux内核视角里根本没有进程和线程两种东西,只有一群 task_struct,谁和谁共享同一套资源对象,谁和谁就是"同一个进程里的线程"。
在Linux下“资源”具体指以下三大类
1. 内存资源(地址空间)
- 进程独有:每个进程都有一张独立的页表(Page Table),映射到不同的物理内存。
- 线程共享:同一个进程里的线程,共用同一张页表。(因此,线程A malloc 出一块内存,线程B 直接能用指针访问)
2.I/O 资源(文件描述符表)
- 进程独有:每个进程维护一张打开文件描述符表,记录着当前打开了哪些文件、socket(网络套接字)、管道。
- 线程共享:线程间共享这张表。( 所以线程A打开 fd= 3,线程B可以直接往 fd=3 写数据)
3. 身份与状态资源(上下文)
- 进程ID(PID/PPID)、用户ID(UID/GID)、工作组(Session ID)。
- 信号处置表(Signal Handlers):比如你对 SIGINT 设置了自定义回调函数。
线程共享这张表,但对于信号的状态表(Pending)线程是独立的。
- 当前工作目录(cwd)和环境变量(environ)。
线程共享进程资源的核心:资源被做成独立的内核对象,线程通过"指针"指向它们,而不是把资源复制一份。
多个线程的 task_struct 里,指向资源的指针,通过引用计数的方式指向同一个对象。
共享的(资源) :
mm、files、sighand...指向同一对象私有的(执行上下文) :寄存器、程序计数器、用户栈、内核栈、TLS、
errno、TID、调度状态......每个线程一份
图示理解线程资源的指向:
线程A的 task_struct 和 线程B的 task_struct
|
├─ mm ────────┐
│ └─────→ 同一个 mm_struct(引用计数 = 2) (同一份页表)
│
├─ files ─────┐
│ └─────→ 同一个 files_struct(引用计数 = 2) (同一张文件描述符表)
│
└─ sighand ───┐
└─────→ 同一个 sighand_struct(引用计数 = 2) (同一张信号处理表)
1.3 线程中私有的资源
1.3.1 CPU 寄存器 ---硬件上下文
当线程在运行时:这些硬件上下文数据放在 CPU 中;
当线程被切换时:内核把它们保存到该线程的内核栈(用PCB进行维护) 中,切回来时进行恢复。
- 程序计数器 PC:我执行到哪条指令了
- 栈指针 SP:指向我的用户栈
- 通用寄存器:计算中间的结果
- 标志寄存器、浮点/向量寄存器
1.3.2 虚拟内存 ---栈
- 用户栈:局部变量、函数参数、返回地址全在这里,局部变量。
·对于主线程的用户栈,位于虚拟地址空间的栈区,能够动态增长,在可执行程序加载时进行分配。
·对于子线程的用户栈,位于虚拟地址空间的共享区,大小固定,由mmap进行申请
·用户栈通过task_struct中的mm_struct结构体和vm_area_struct进行管理。
- 内核栈:线程通过系统调用进入内核时,走自己独立的内核栈。
·在内核空间中都有的固定大小的栈。
·内核栈通过task_struct中的stack指针进行管理
- TLS(线程局部存储): 通过__thread/ thread_local 修饰的变量 和 errno 变量。
·每个线程的 fs 寄存器指向自己那一块 TLS,所以同一行代码里写 errno,各线程读写的是各自的副本。
1.3.3 task_struct ---部分字段
- LWP :线程身份ID
- 调度状态(运行/睡眠)、优先级、nice 值、CPU 亲和性、调度策略
- 信号屏蔽字 blocked:线程选择挡住哪些信号
- 线程私有的 pending 信号
1.3.4 总结
线程拥有独立的:
1. 寄存器
2. 程序计数器 PC
3. 线程栈 Stack
4. 线程局部存储 TLS
5. 调度状态
线程共享进程的:
1. 代码段
2. 全局变量
3. 静态变量
4. 堆 Heap
5. 文件描述符
6. Socket
7. 进程资源
二、Linux 线程控制
2.2 pthread_create - 创建线程
2.2.1 基础使用要求
1.头文件包含: #include <pthread.h>
2.函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
3.函数功能:创建出一个新的线程,产生一个新的执行流。
注:
A. 编译链接参数:
pthread不是 Linux 默认链接的库,编译时需要显式添加-lpthread或-pthread参数,否则会报undefined reference to pthread_create的链接错误。B. 错误处理规则:与常规系统调用不同,
pthread_create失败时不会设置全局errno,而是直接返回对应的错误码,需要用strerror(ret)将错误码转换为可读的错误信息,不能使用perror()。
2.2.1 参数说明
1.参数一:pthread_t *thread
- 指向 pthread_t 类型变量的指针,用于接收新创建线程的唯一标识符(线程 ID)。
- 后续对该线程的所有操作(如等待结束、分离、取消等)都需要通过这个 ID 进行。
2.参数二:const pthread_attr_t *attr (一般可忽略)
- 栈大小:默认在 x86_64 架构下为 2MB,可通过 pthread_attr_setstacksize() 修改
- 分离状态:默认可连接(joinable),需要调用 pthread_join() 回收资源;也可设置为分离(detached),线程结束后自动释放资源
- 调度策略:如 SCHED_FIFO(实时先进先出)、SCHED_RR(实时轮转)等,默认为普通分时调度 SCHED_OTHER
3.参数三:start_routine(线程入口函数)
函数头必须按照该格式设计:void* 函数名 (void * arg) ,线程执行的入口部分。
4.参数四: void *arg (传参给线程入口函数)
- 传递给 start_routine 的唯一参数,类型为 void*。
- 如果需要传递多个参数,必须将所有参数封装到结构体中,传递结构体的地址。
2.2.2 函数返回值
成功:返回 0,此时 thread 指向的变量会被设置为新线程的 ID。
失败:返回非零的错误码,thread 指向的内容未定义,不会创建新线程。
注:需要通过strerror进行输出错误信息。
函数的错误码:
| 错误码 | 含义 |
|---|---|
EAGAIN |
系统资源不足,无法创建新线程(如超过线程数上限、内存不足等) |
EINVAL |
attr 属性配置无效,或 start_routine 为 NULL |
EPERM |
没有权限设置 attr 中指定的调度策略或参数(如实时调度需要 root 权限) |
2.2.3 代码演示
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void *thread_run(void *args)
{
int cnt = 5;
while (cnt--)
{
printf("%s starts running...\n",name);
}
return nullptr;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, nullptr, thread_run, (void *)"thread -1");
if (ret != 0)
{
strerror(ret);
}
int cnt = 5;
while (cnt--)
{
printf("Main thread running...\n");
}
return 0;
}
运行结果如下所示:
Main thread running...
Main thread running...
Main thread running...
Main thread running...
Main thread running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
thread -1 starts running...
2.3 pthread_join -线程等待
2.3.1 基础使用条件
1.头文件包含 #include <pthread.h>
2.函数原型:int pthread_join(pthread_t thread, void **retval);
3.函数功能:同步和回收可连接(joinable)的线程资源
2.3.2 参数说明
1.参数一:pthread_t thread
- 目标线程的 ID,即 pthread_create 第一个输出参数获得的值。
- 必须是当前进程中有效的、尚未被 join 且未分离的线程 ID。
2.参数二:void **retval
- 指向 void* 指针的二级指针,用于接收目标线程通过 return 或 pthread_exit() 传递的返回值。
- 如果不需要获取返回值,传 NULL 即可。
- 如果目标线程是被 pthread_cancel() 取消的,*retval 会被设置为特殊常量 PTHREAD_CANCELED。
注意:绝对不要将局部变量的地址作为返回值传出!因为线程结束后其栈帧已被销毁,该地址变为悬空指针,访问它将导致未定义行为,应使用堆内存、全局变量或静态变量来传递复杂数据。
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
void* worker(void* arg)
{
int input = *(int*)arg;
int* result = malloc(sizeof(int)); // 堆上分配,安全
*result = input * input;
return result;
}
int main()
{
pthread_t tid;
int val = 7;
pthread_create(&tid, NULL, worker, &val);
void* ret;
int rc = pthread_join(tid, &ret);
if (rc != 0)
{
printf("join failed: %s\n", strerror(rc));
return 1;
}
printf("7^2 = %d\n", *(int*)ret);
free(ret); // 记得释放堆内存
return 0;
}
2.4 pthread_detach -线程分离
2.4.1 基础使用条件
1.头文件包含: #include <pthread.h>
2.函数原型:int pthread_detach(pthread_t thread);
3.函数功能:将指定线程标记为"分离状态(detached)",分离线程终止时,系统自动回收其资源(栈、线程描述符等),无需也不允许其他线程调用 pthread_join。
2.4.2 detach后的线程状态
操作系统detach线程的执行过程
1. 立即释放线程栈内存(通常几 MB)
2. 释放线程控制块(TCB)
3. 丢弃返回值(即使线程函数 return 了非 NULL 值,也无处接收)
4. 不保留任何可供 join 的状态
2.4.3 detach的两种时机
A. 运行时 detach
pthread_t tid;
pthread_create(&tid, NULL, worker, arg);
pthread_detach(tid); // 创建后分离
B. 创建前 detach
pthread_attr_t attr;//线程属性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t tid;
pthread_create(&tid, &attr, worker, arg); // 出生即分离
pthread_attr_destroy(&attr); // 属性对象用完必须销毁,否则泄漏
C. detach 时间线分析
方式A的时间线:
create ──────────────── detach
↑ 危险窗口 ↑
线程可能在此区间内已经终止!
如果线程在 pthread_create 和 pthread_detach 之间就执行完毕并退出了,而系统又恰好立即回收了其资源,那pthread_detach(tid) 操作的就是一个无效的线程标识符,属于未定义行为。
方式B的时间线:
create(DETACHED) ───→ 线程从第一刻起就是分离状态 不存在竞态窗口
2.4.4 detach 的约束
A. 绝不能对分离线程调用 join
pthread_detach(tid);
pthread_join(tid, NULL); // 明确规定这是未定义行为
B. 不能获取返回值
void* worker(void* arg)
{
return some_result; // 如果线程是 detached,这个返回值被丢弃
}
C. 参数生命周期必须自给自足
// 经典错误
void * worker(void * args)
{
//...执行任务
return NULL;
}
void spawn_task(int id)
{
int local_id = id;
pthread_create(&tid, &attr, worker, &local_id); // 传栈变量地址
// spawn_task 返回后 local_id 销毁
// 分离线程可能还在运行,此时读到悬空指针
}
在 pthread_join模式下,主线程通过等待隐式延长了参数的生命周期。
detach 模式下没有这个安全机制,spawn_task函数结束后,局部变量销毁,导致线程中的指针悬空。
2.5 线程终止
线程终止:是线程生命周期的最后一个环节,也是资源回收的前置条件。
POSIX 提供了三种终止方式,它们的语义、副作用和适用场景各不相同。
2.5.1 线程执行 return
基本语义:线程入口函数void*(*)(void*)执行 return 语句,是最自然的终止方式。
void* worker(void* arg)
{
int id = *(int*)arg;
printf("Thread %d doing work...\n", id);
// 正常执行完毕,return 一个值
return (void*)(long)id; // 将整数转为 void* 返回
}
关键细节
细节一:return 的值就是 pthread_join 能取到的值
void* worker(void* arg)
{
int id = *(int*)arg;
printf("Thread %d doing work...\n", id);
// 正常执行完毕,return 一个值
return (void*)(long)id; // 将整数转为 void* 返回
}
void* retval;
pthread_join(tid, &retval); //二级指针进行获取
printf("Thread returned: %ld\n", (long)retval);
细节 2:return NULL 表示"无返回值",不是"失败"
void* worker(void* arg)
{
// 做一些事情...
return NULL; // 正常终止,没有需要传递的结果
}
NULL和PTHREAD_CANCELED是不同的,join 可以通过这个区分判断线程是"自然死亡"还是"被杀死"。
NULL表示线程自行决定正常结束;
PTHREAD_CANCELED是系统标记,表示线程被外部取消。
细节 3:绝不能返回局部变量的地址
//错误做法:
void* worker(void* arg)
{
char buf[128];
snprintf(buf, sizeof(buf), "result_%d", *(int*)arg);
return buf; // 致命错误!buf 在 return 后栈帧销毁
}
//正确做法:
// 方案A:堆分配
char* buf = malloc(128);
snprintf(buf, 128, "result_%d", *(int*)arg);
return buf; // 调用方负责 free
// 方案B:静态/全局变量(不推荐,非线程安全)
static char buf[128]; // 多线程同时 return 会覆盖
// 方案C:返回整数值(最安全)
return (void*)(long)some_int;
细节 4:return 只终止当前线程,不影响其他线程
void* worker(void* arg)
{
return NULL; // 仅本线程结束
// main 线程和其他子线程继续运行
}
细节 5:C++ 中 return 会触发栈上对象的析构
void* worker(void* arg)
{
std::string s = "hello";
std::lock_guard<std::mutex> lock(mtx);
// ... 做一些事情 ...
return NULL;
// s 的析构函数被调用
// lock 的析构函数被调用(mutex 被解锁)
// 所有 RAII 对象正常清理
}
2.5.2 线程执行 pthread_exit
函数原型:
头文件包含:#include <pthread.h>
函数原型:void pthread_exit(void *retval);
核心用法:显式终止调用它的线程。
retval的含义与return完全一致------都是传递给pthread_join的返回值。
void* worker(void* arg)
{
int id = *(int*)arg;
if (some_error_condition) {
fprintf(stderr, "Thread %d encountered error, exiting early.\n", id);
pthread_exit((void*)-1); // 提前终止,不执行后续代码
}
// 正常逻辑...
return (void*)(long)id;
}
细节分析:
POSIX 标准的规定 :pthread_exit 应当触发 C++ 栈展开(stack unwinding),即调用析构函数。但实际行为依赖编译器和平台:
-
Linux glibc + g++ :通常正确触发析构(通过内部
__cleanup机制) -
某些嵌入式/旧编译器 :可能直接
longjmp,跳过析构。 -
C++ 标准本身 :不承认
pthread_exit的存在,行为是实现定义的。
void* worker(void* arg)
{
std::string s = "important data";
std::lock_guard<std::mutex> lock(mtx);
helper_function(); // 内部调用了 pthread_exit
// 问题:s 和 lock 的析构函数是否被调用?
}
void helper_function()
{
// ... 一些逻辑 ...
pthread_exit(NULL); // 从辅助函数中终止线程
}
2.6 pthread库
2.6.1 线程库的本质定位
线程库的本质:Linux 内核本身没有"线程"这个概念,线程是用户态库(pthread)对内核"轻量级进程"机制的封装。
内核提供"底层结构"(轻量级进程task_struct + futex),pthread 库负责"封装"成标准化的线程接口。
┌─────────────────────────────────┐
│ pthread │ 用户空间:
│ ┌──────────┐ ┌──────────┐ │
│ │ struct │ │ struct │ │ ← 线程控制块(TCB):管理线程属性、栈地址等
│ │ pthread │ │ pthread │ │
│ │ (TCB) │ │ (TCB) │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ 线程栈 │ 线程栈 │
│ ▼ ▼ │
├───────┼─────────────┼───────────┤
│ │ clone() │ clone() │ ← 系统调用边界
├───────┼─────────────┼───────────┤
│ ▼ ▼ │ 内核空间:
│ ┌───────────┐ ┌────────────┐ │
│ │task_struct│ │task_struct │ │ ← 内核调度实体(LWP):cpu的调度单位
│ │ (LWP) │ │ (LWP) │ │
│ └───────────┘ └────────────┘ │
│ │
└─────────────────────────────────┘
2.6.2 线程地址空间布局
线程地址空间布局图:
┌─────────────────────┐ Highest addresses
│ │
| Kernel space |
| |
┌─────────────────────┐
| Stack |
| |
│ Main thread stack │ ← 主线程栈
├─────────────────────┤
│ mmap shared area │ ← TCBs + thread stacks for other threads are here
│ ┌───────────────┐ │ 其他线程的 TCB + 线程栈都在这里
│ │ TCB (Thread 2)│ │
│ │ Stack of Thd 2│ │
│ ├───────────────┤ │
│ │ TCB (Thread 3)│ │
│ │ Stack of Thd 3│ │
│ └───────────────┘ │
| ... │
├─────────────────────┤
│ Heap (shared) │ ←堆区
├─────────────────────┤
│ Data segment │ ←数据段
├─────────────────────┤
| Code segment | ←代码段
└─────────────────────┘ Low addresses 低地址

2.6.3 内核空间管理线程
Linux 内核对进程和线程都用 task_struct 表示,每个线程在内核中都有一个独立的 task_struct,这就是一个轻量级进(LWP)。
task_struct 中的关键字段:
struct task_struct
{
//线程的ID
pid_t pid; // 内核线程 ID(每个 LWP 唯一)
pid_t tgid; // 线程组 ID(同进程所有线程共享)
// 管理线程
struct task_struct *group_leader; // 指向主线程
struct list_head thread_group; // 线程组链表
struct mm_struct *mm; // 地址空间描述符(同组线程共享)
// 线程的上下文
// ... 寄存器、调度优先级、状态等
};
pid:内核视角的"进程 ID",对 LWP 来说就是线程 ID(通过gettid()获取)
tgid:线程组 ID,等于主线程的pid(getpid()返回的就是tgid)
mm:指向虚拟内存描述符,同组线程共享同一个mm,因此共享地址空间
2.6.4 用户空间管理线程
┌──────────────────────────────┐ 高地址
│ struct pthread(TCB) │ ← pthread_t 就是这个结构体的首地址
├──────────────────────────────┤
│ 线程局部存储(TLS) │ ← __thread 变量的副本
├──────────────────────────────┤
│ │
│ 线程栈(默认 8MB) │ ← 向下增长
│ │
└──────────────────────────────┘ 低地址
High addresses 高地址
┌──────────────────────────────┐
│ struct pthread (TCB) │ ← 线程描述表:pthread_t 就是这个结构体的首地址
├──────────────────────────────┤
│ TLS │ ← 线程局部存储: 存储__thread 变量的副本
├──────────────────────────────┤
│ │
│ Thread stack │ ← 线程栈: 向下增长
│ (default 8MB) │
└──────────────────────────────┘
Low addresses 低地址
A.struct pthread -TCB
struct pthread
{
/* ---- 标识信息 ---- */
struct pthread *self; // ★指向自身(用于 %fs:0 快速定位)
pid_t tid; // ★内核分配的 LWP ID(gettid 的值)
pid_t pid; // ★进程 ID(tgid)
/* ---- 线程栈信息 ---- */
void *stackblock; // ★栈内存的起始地址
size_t stackblock_size; // 栈大小(默认 8MB)
void *guardaddr; // 栈底保护页地址(防止栈溢出)
size_t guardsize; // 保护页大小(默认 4KB)
/* ---- 生命周期状态 ---- */
int cancel_state; // 取消状态(是否允许被取消)
int cancel_type; // 取消类型(延迟/异步)
int canceled; // 是否已被取消
int detached; // 是否已 detach
int joinid; // 谁在 join 我
/* ---- 同步相关 ---- */
struct pthread_mutex *mutex_list; // 持有的互斥锁链表
futex_t *robust_list; // 健壮互斥锁列表
/* ---- 清理与退出 ---- */
struct _pthread_cleanup_buffer *cleanup; // 清理处理器链表
void *(*start_routine)(void *); // 线程入口函数
void *arg; // 入口函数参数
void *result; // ★线程返回值(pthread_exit 的参数)
/* ---- 信号相关 ---- */
sigset_t sigmask; // 线程信号掩码
sigset_t sigpending; // 挂起信号集
/* ---- TLS 相关 ---- */
void *specific[LEVEL1_SIZE]; // pthread_key_create 的动态 TLS 数组
dtv_t *dtv; // 动态线程向量(Dynamic Thread Vector)
/* ---- errno ---- */
int err_no; // 本线程的 errno(通过 TLS 访问)
/* ---- 调度信息 ---- */
int schedpolicy; // 调度策略
int schedparam; // 调度参数
int flags; // 线程标志位
/* ... 更多字段 ... */
};
关键字段:
| 字段 | 作用 |
|---|---|
self |
放在 TCB 的第一个位置,%fs:0 直接读到 TCB 自身地址。 |
tid |
创建时由内核通过 CLONE_CHILD_SETTID 写入,是用户态访问内核 LWP 的唯一凭证 |
stackblock |
记录栈的起始地址,线程退出时据此释放内存 |
dtv |
动态线程向量,管理动态链接库中声明的 __thread 变量 |
specific[] |
pthread_key_create() 创建的动态 TLS 槽位 |
cleanup |
pthread_cleanup_push() 注册的清理函数链表 |
result |
pthread_exit() 或线程函数返回值,供 pthread_join() 获取 |
B. 线程局部存储 -TLS
TLS解决的问题:全局变量和静态变量在所有线程间共享,但是很多场景需要"看起来像全局变量,实际上每个线程各有一份"的数据。
例如:
errno、日志缓冲区、线程私有缓存等。
关键字:__thread 和 thread_local
// C 语言(GCC 扩展)
__thread int counter = 0; //每个线程都能看见,但是y独立的一份
// C++11 标准
thread_local std::string buffer;
C. 线程栈 -Thread stack
线程栈存放的内容如下所示:
┌─────────────────────────────────────┐ Higher addresses
│ Function arguments │ ← 函数参数
├─────────────────────────────────────┤
│ Return address (pushed by call) │ ← 返回地址(call 指令压入)
├─────────────────────────────────────┤
│ Saved registers (RBP, RBX, etc.) │ ← 保存的寄存器(RBP、RBX 等)
├─────────────────────────────────────┤
│ Local variables (arrays, structs) │ ← 局部变量(数组、结构体等)
├─────────────────────────────────────┤
│ Temporary values (intermediate) │ ← 临时值(函数调用的中间结果)
└─────────────────────────────────────┘
线程栈的介绍:
- 用户栈:局部变量、函数参数、返回地址全在这里,局部变量。
·对于主线程的用户栈,位于虚拟地址空间的栈区,能够动态增长,在可执行程序加载时进行分配。
·对于子线程的用户栈,位于虚拟地址空间的共享区,大小固定,由mmap进行申请
·用户栈通过task_struct中的mm_struct结构体和vm_area_struct进行管理。
- 内核栈:线程通过系统调用进入内核时,走自己独立的内核栈。
·在内核空间中都有的固定大小的栈。
·内核栈通过task_struct中的stack指针进行管理
2.6.5 线程的完整流程
A.线程创建的完整流程
用户空间 内核空间
│ │
│ 1. mmap() 分配线程栈 + TLS │
│ 2. 初始化 TCB (struct pthread) │
│ 3. 调用 clone() 系统调用 ──────────→│
│ flags: |
| CLONE_VM | 4. 创建 task_struct
│ CLONE_FILES │ 5. 根据 flags 共享资源:
│ CLONE_SIGHAND │ - CLONE_VM → 共享 mm(地址空间)
│ ... │ - CLONE_FILES → 共享 files_struct(文件描述符表)
│ │ - CLONE_SIGHAND → 共享 signal_struct(信号处理)
| │ ...
| │
│ │ 6. 设置 tgid = 父线程 tgid
│ │ 7. 设置 group_leader
│ │ 8. 加入调度队列
│ ←──────── 返回新线程的 LWP ID ──── │
│ 9. 记录 TCB 与 LWP 的映射 │
│ │
│ │ 10. 新线程被调度后开始执行 start_routine
│ │
B.线程销毁的完整流程
用户空间 内核空间
│ │
│ 1. 线程函数返回,调用pthread_exit() │
│ │
│ 2. 执行清理处理器 │
│ 3. 调用 exit() 系统调用 ──────────→ │
│ │ 4. 内核释放 task_struct
│ │ 5. 如果设置了 CLONE_CHILD_CLEARTID,
│ │ 清零 TCB 中的 tid 字段
│ │
│ ←──────────────────────────────── │ 6. futex 唤醒等待 join 的线程
| 7. join 线程被唤醒,获取返回值 |
│ 8. 回收线程栈和 TCB(mmap 释放) │
2.7 thread的封装
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cstring>
#include <cstdlib>
// 基于 C 接口 (pthread) 的 C++ 线程封装
namespace ThreadModule
{
// 已绑参的可调用对象类型: 无参无返回
using func_t = std::function<void()>;
// 内部上下文: 承载可调用对象 + 线程名
// 独立分配在堆上, 是为了把生命周期与 Thread 对象解耦
// (线程可能在 Thread 对象析构后仍在运行, 不能让回调引用悬空)
class ThreadData
{
friend class Thread; // 须带 class: 否则 Thread 尚未声明, 会被当成友元函数声明
public:
ThreadData(const func_t &func, const std::string &name)
: _func(func),
_name(name)
{
}
private:
func_t _func;
std::string _name;
};
// 线程状态: 防止非法操作 (重复 join / join 已 detach 的线程等)
enum class ThreadStatus
{
NEW, // 已构造, 未启动
RUNNING, // 运行中
DETACHED, // 已分离
JOINED // 已回收
};
// 线程自动编号: inline 函数 + static 局部变量
// C++11 下 inline 函数的 static 局部量跨编译单元共享, 头文件安全
inline int NextThreadId()
{
static int id = 1;
return id++;
}
class Thread
{
public:
// 接受任意可调用对象 + 参数, 用 std::bind 绑参后转为 std::function<void()>
template <typename F, typename... Args>
Thread(const std::string &name, F &&f, Args &&...args)
{
_name = name + "-" + std::to_string(NextThreadId());
_status = ThreadStatus::NEW;
_td = new ThreadData(
std::bind(std::forward<F>(f), std::forward<Args>(args)...),
_name);
}
~Thread()
{
// 析构策略 B : 仍在运行就自动 detach, 保证不泄漏
if (_status == ThreadStatus::RUNNING)
{
pthread_detach(_tid); // 不断言, 容错 (线程可能已结束)
}
// 从未启动: 跳板不会运行, 自己清理 _td
if (_status == ThreadStatus::NEW && _td != nullptr)
{
delete _td;
_td = nullptr;
}
// 已启动 (RUNNING/DETACHED/JOINED): _td 由跳板结束时释放, 此处不动
}
// 禁用拷贝: 一个 pthread_t 不能被两个对象管理 (double join / double free)
Thread(const Thread &) = delete;
Thread &operator=(const Thread &) = delete;
// 启动线程 (构造与启动分离, 便于统一管理一批线程)
void start()
{
if (_status != ThreadStatus::NEW)
{
std::cerr << "[Thread:" << _name << "] already started\n";
return;
}
int n = pthread_create(&_tid, nullptr, start_routine, _td);
if (n != 0)
{
std::cerr << "pthread_create error: " << strerror(n) << "\n";
std::abort();
}
_status = ThreadStatus::RUNNING;
}
// 等待回收
void join()
{
if (_status != ThreadStatus::RUNNING)
{
std::cerr << "[Thread:" << _name << "] cannot join (status not RUNNING)\n";
return;
}
int n = pthread_join(_tid, nullptr);
if (n != 0)
{
std::cerr << "pthread_join error: " << strerror(n) << "\n";
std::abort();
}
_status = ThreadStatus::JOINED;
_td = nullptr; // _td 已被跳板释放, 仅置空标记
}
// 分离
void detach()
{
if (_status != ThreadStatus::RUNNING)
{
std::cerr << "[Thread:" << _name << "] cannot detach (status not RUNNING)\n";
return;
}
int n = pthread_detach(_tid);
if (n != 0)
{
std::cerr << "pthread_detach error: " << strerror(n) << "\n";
std::abort();
}
_status = ThreadStatus::DETACHED;
_td = nullptr; // _td 由跳板结束时释放
}
const std::string &name() const { return _name; }
bool isRunning() const { return _status == ThreadStatus::RUNNING; }
private:
// 跳板: 签名必须匹配 pthread 要求的 void*(*)(void*)
// 由于非 static 成员函数隐含 this, 签名不匹配, 因此必须用 static
static void *start_routine(void *arg)
{
ThreadData *td = static_cast<ThreadData *>(arg);
td->_func(); // 执行用户回调
delete td; // 跳板作为最终消费者, 释放上下文
return nullptr;
}
private:
pthread_t _tid;
std::string _name; // 自己保留一份, name() 在 join/detach 后仍可用
ThreadStatus _status;
ThreadData *_td = nullptr;
};
} // namespace ThreadModule