1.线程概念
线程是进程内的最小执行单元,一个进程可以包含多个线程,所有线程共享进程的资源(内存、文件句柄等),但有自己独立的执行栈和程序计数器。
结合进程的核心区别可以这样理解:
进程是资源分配的基本单位
系统给进程分配内存、CPU 时间片等资源,进程像是一个 "独立的工作车间"。
线程是 CPU 调度的基本单位
线程是车间里的 "工人",一个车间可以有多个工人,他们共享车间的工具和材料(进程资源),但各自干不同的活,且切换工人的开销远小于切换车间。
要是想使用线程,就得自己重新定义线程的结构体,相当于从头重新弄一套线程相关的实现;而 Linux 那边好像是直接用了原本就有的线程那一套机制,把它叫做轻量级进程
wiindows 系统就算重新弄了一套相关的实现
我们可以简单理解成
线程:是进程内的一个执行分支
如何理解我们以前的进程???
操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只有一个线程(主线程)!
Linux 实现方案
在 Linux 中,线程在进程 "内部" 执行,线程在进程的地址空间内运行
任何执行流要执行,都要有资源!地址空间是进程的资源窗口
2.线程的性质
同一进程内的多个线程:共享内核空间的数据页表(且共享用户空间页表,因线程共享进程的页表根目录 PGD,内核空间映射全局统一);
不同进程的线程:逻辑上共享内核空间页表(所有进程的内核空间页表映射完全一致,系统启动时初始化一次,全局复用),但用户空间页表相互独立。
我们还是用以前虚拟地址的图片

|--------|----------|---|---|---|
| | 相同进程不同线程 | 不同进程不同线程 |||
| 内核空间页表 | 相同 | 相同 |||
| 用户空间页表 | 相同 | 不同 |||
假设我们创建了n个进程
内核空间页表只有1份
用户空间页表有n份
无论一个进程里面有几个线程 一个进程里面只有一份独立的用户空间页表
和一份和其他进程共享的一份内核空间页表
线程切换比进程快 为什么???
线程共享进程的地址空间、文件等资源,切换时不用换页表、刷新缓存(这些是进程切换的大开销);
仅需保存 / 恢复 CPU 执行状态(比如寄存器、程序计数器),不用处理进程级的资源上下文。
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该 进程内的所有线程也就随即退出

3.虚拟地址到物理地址的转换方式

高 10 位(PDE 索引)= 楼栋号
小区太大,先按 "楼栋" 分组(页目录就是小区的「楼栋列表」,CR3 寄存器是小区大门的指引牌,告诉你楼栋列表放在小区哪个角落)。
高 10 位就是你快递上的 "楼栋号",用它查楼栋列表,就能找到目标快递所在的楼栋(对应 "找到目标页表的物理地址")。
中 10 位(PTE 索引)= 楼层号
找到楼栋后,还要找具体楼层(页表就是这栋楼的「楼层列表」)。
中 10 位是 "楼层号",用它查楼层列表,就能找到目标快递所在的那一层(对应 "找到目标物理页帧的地址")。
低 12 位(页内偏移)= 房间号
每一层的户型大小都一样(都是 4KB,因为 2^12=4096 字节),房间号范围固定(0-4095)。
低 12 位直接就是 "房间号",不用再查列表,直接顺着楼层找到对应的房间(对应 "物理页帧内的具体字节位置")。
如果不拆分,直接用 32 位虚拟地址对应物理地址,会有两个大问题:
浪费内存:
32 位系统最大虚拟地址是 4GB,按 4KB 一页算,需要 100 万个 "页表项"(4GB/4KB=1024*1024),每个页表项占 4 字节,一个进程的页表就占 4MB。如果有 1000 个进程,光页表就占 4GB,内存直接炸了!
拆分后(10+10+12):
页目录只需要 1024 个项(10 位),占 4KB(1024*4 字节);
每个页表也只需要 1024 个项,占 4KB;
只有进程用到的页表才会加载到内存,不用一次性加载所有页表,比如一个进程只用到 10 个页,总页表占用才 4KB(页目录)+10*4KB(页表)=44KB,比 4MB 省了近 100 倍!
查找更快:
拆分后是 "三级索引"(页目录→页表→页内偏移),每级都是固定长度的索引(10 位、10 位、12 位),CPU 的 MMU(内存管理单元)能像 "查字典按部首→页码→行数" 一样,硬件级快速计算地址,比线性查找快得多。
4.线程接口
线程函数的接口统一的一个头文件
cpp
#include <pthread.h>
1.pthread_create
作用是启动一个新的执行流(线程)。
cpp
int pthread_create(
pthread_t *thread, // 输出:新线程的ID(类似进程PID)
const pthread_attr_t *attr, // 线程属性:NULL表示用默认属性
void *(*start_routine)(void *), // 线程要执行的函数(函数指针)
void *arg // 传递给start_routine的参数
);
thread:传入一个pthread_t类型的指针,函数会把新线程的 ID 写入这个指针指向的变量。
attr:线程属性(如栈大小、调度策略),一般填NULL用默认属性。
start_routine:线程要执行的函数,必须是void* (*)(void*)类型(入参是void*,返回值是void*)。
arg:传递给start_routine的参数,若要传多个参数,需封装成结构体指针。
返回值:成功返回0;失败返回错误码(不是-1,需用strerror()查看错误信息)。
2. pthread_join
cpp
int pthread_join(
pthread_t thread, // 要等待的子线程ID(由pthread_create生成)
void **retval // 输出:存储子线程的返回值;不需要则填NULL
);
thread:目标子线程的 ID(pthread_t类型,由pthread_create写入)。
retval:二级指针,用于接收子线程return的void*返回值;若无需返回值,填NULL。
返回值:成功返回0;失败返回错误码(非-1,可用strerror()查看错误信息)。
阻塞等待:调用pthread_join的线程(通常是主线程)会阻塞,直到目标子线程执行完毕。
回收资源:子线程结束后,若不调用pthread_join,其资源(如线程控制块 TCB、栈)不会自动释放,会成为 "僵尸线程",导致资源泄露。
获取返回值:可以拿到子线程的执行结果(子线程通过return返回的void*数据)。
3.pthread_self()
获取当前线程 ID
cpp
pthread_t pthread_self(void);
返回值
当前线程的 pthread_t 类型 ID。
4.pthread_exit()
cpp
void pthread_exit(void *retval);
前面我们学过的exit和_exit是进程的主动退出方式
pthread_exit()是线程的主动退出方式
立即终止调用线程,将 retval 作为线程的退出状态(可被 pthread_join 获取)。
retval:线程退出的返回值指针(不能指向线程栈上的局部变量,因为线程退出后栈会被销毁)。
返回值
无(线程已终止,不会返回)。
线程函数中可显式调用 pthread_exit 退出,也可通过 return 返回(效果等价,return 的返回值会被当作 pthread_exit 的 retval)。
主线程特殊行为:主线程调用 pthread_exit 后,主线程终止,但进程不会退出,其他子线程会继续运行(区别于 exit(),exit() 会终止整个进程)。
!!!注意!!!
主线程从main函数return时,其他线程会被终止,但这一现象的本质并非 "主线程 return 直接终止子线程",而是主线程 return 触发了进程退出,进程退出会导致所有线程被内核终止。
普通子线程的return 该线程结束,返回退出状态 继续运行 不受影响,正常运行
无论是子线程还是主线程调用exit的结果都是一样的 进程退出
5.pthread_detach()
cpp
int pthread_detach(pthread_t thread);
设置线程为分离状态
将指定线程设置为分离状态(detached),线程退出后,其占用的系统资源会被内核自动回收,无需调用 pthread_join 等待。
thread:需要设置为分离状态的线程 ID。
返回值
成功:返回 0;
失败:返回非 0 的错误码(如 EINVAL:线程无效或已分离;ESRCH:线程不存在)。
主线程中调用:pthread_detach(tid)(tid 是创建的线程 ID);
线程内部调用:pthread_detach(pthread_self())(推荐,线程自身控制分离状态);
线程一旦被分离,无法再被 pthread_join 等待,调用 pthread_join 会返回 EINVAL 错误。
不能对已被 pthread_join 的线程调用 pthread_detach(会失败)。
若线程未被分离且未被 pthread_join,退出后会变成僵尸线程,占用系统资源直到进程退出。
5.pthread_cancel()
cpp
int pthread_cancel(pthread_t thread);
发送线程取消请求
向指定线程发送取消请求,请求线程终止运行。线程是否响应、何时响应取决于其取消状态和取消类型。
参数
thread:需要取消的线程 ID。
返回值
成功:返回 0;
失败:返回非 0 的错误码(如 ESRCH:线程不存在)。
pthread_cancel() 仅发送请求,并非立即终止线程。
线程响应取消后,会返回 PTHREAD_CANCELED(宏定义,值为 (void*)-1),可被 pthread_join 获取。
++我们可以写三个代码用来测试这些函数++
cpp
#include <stdio.h>
#include <pthread.h>
void *thread_work(void *arg) {
printf("【子线程】启动\n");
unsigned long tid = (unsigned long)pthread_self();
printf("【pthread_self()】执行成功,子线程自身ID:%lu\n", tid);
printf("【pthread_exit()】即将执行,执行后子线程终止,后续代码不会运行\n");
pthread_exit(NULL);
printf("【pthread_exit()】未生效(此行不该打印)\n");
}
int main() {
pthread_t tid;
printf("【主线程】pthread_create()即将创建子线程\n");
pthread_create(&tid, NULL, thread_work, NULL);
printf("【pthread_create()】执行成功,创建的子线程ID:%lu\n", (unsigned long)tid);
printf("【主线程】pthread_join()即将等待子线程结束\n");
pthread_join(tid, NULL);
printf("【pthread_join()】执行成功,子线程已结束\n");
return 0;
}

cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程工作函数(极简版)
void* thread_func(void* arg) {
char* name = (char*)arg;
for (int i = 1; i <= 3; i++) { // 循环3次,简化次数
printf("线程[%s]:第%d次执行\n", name, i);
sleep(1); // 取消点:响应cancel
}
printf("线程[%s]:执行完毕\n", name); // 被取消则不执行
return NULL;
}
int main() {
pthread_t t1, t2;
// 创建线程t1(不取消)、t2(将取消)
pthread_create(&t1, NULL, thread_func, "t1(不取消)");
pthread_create(&t2, NULL, thread_func, "t2(将取消)");
sleep(2); // 让t2执行2次后取消
pthread_cancel(t2); // 发送取消请求
// 等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}

cpp
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
// 线程工作函数(极简版)
void* thread_func(void* arg) {
printf("线程[%s]执行\n", (char*)arg);
sleep(1); // 模拟工作
printf("线程[%s]结束\n", (char*)arg);
return NULL;
}
int main() {
pthread_t t1, t2;
int ret;
// 创建非分离线程t1、分离线程t2
pthread_create(&t1, NULL, thread_func, "t1(未分离)");
pthread_create(&t2, NULL, thread_func, "t2(已分离)");
pthread_detach(t2); // 设置分离
// 尝试join非分离线程t1(成功)
ret = pthread_join(t1, NULL);
printf("join t1:%s\n", ret ? strerror(ret) : "成功");
// 尝试join分离线程t2(失败)
ret = pthread_join(t2, NULL);
printf("join t2:%s(预期失败)\n", ret ? strerror(ret) : "成功");
sleep(1);
return 0;
}

5.修饰符__thread
其核心作用是让被修饰的变量成为每个线程的私有副本,而非进程级的共享变量。
___thread只能对内置类型使用 不能对自定义类型使用
1.__thread定义在线程函数内时,主线程是否有副本,完全取决于主线程是否执行了这个定义了__thread变量的函数------ 只有执行了函数,才会触发副本的分配;未执行则无副本。
2.__thread定义在全局时 主线程必定拥有副本,这是进程启动的默认行为,与是否使用变量无关;
所有子线程创建时会自动获得独立副本,初始化值为编译期常量;

cpp
#include <stdio.h>
#include <pthread.h>
// 普通全局变量(共享)
int s = 0;
// __thread变量(线程私有)
__thread int t = 0;
// 线程函数:仅做一次自增+打印(精简核心逻辑)
void* f(void* arg) {
int id = *(int*)arg;
s++, t++; // 共享变量自增,私有变量自增
printf("线程%d: s=%d, t=%d\n", id, s, t);
return NULL;
}
int main() {
pthread_t a, b;
int id1=1, id2=2;
pthread_create(&a, NULL, f, &id1);
pthread_create(&b, NULL, f, &id2);
pthread_join(a, NULL);
pthread_join(b, NULL);
printf("主线程: s=%d, t=%d\n", s, t); // 主线程的t是独立副本
return 0;
}

6.线程管理
我们学语言的时候知道 临时变量是有生命周期的 是位于栈上的
对于线程同样适用
主线程和子线程一样有它们的栈区 但是子线程和主线程栈的位置在进程地址空间的不同地方
主线程的栈区位于进程地址空间中的用户地址空间中的栈区
而线程为了方便统一管理 被封装到一个库里面 运行的时候会被加载到内存的共享区
子进程的栈区位于进程地址空间中的用户地址空间中的mmap即共享区(暂时先这么理解)
这张图右侧的struct pthread对应的是用户态的线程控制块(TCB)主线程的tcb也在其中
但是主线程的栈是特殊的(位于进程默认栈区)------ 主线程的 TCB(struct pthread)虽然在 mmap 区,但它记录的主线程栈地址是「进程默认栈区」,

那么看到前面代码的运行结果
你不好奇为什么线程的tid这么大呢
线程在里面叫轻量化进程 进程有pid 线程同样也有tid
每一个线程的库级别的 tcb 的起始地址,叫做线程的 tid!!
线程在里面叫轻量化进程 进程有pcb 线程同样也有tcb
|----------------|--------------------------------------------|------------------------------------------------------------|
| 维度 | 主线程 ID | 子线程 ID |
| 用户态(pthread_t) | 类型为pthread_t,指向主线程的 TCB 地址 | 类型为pthread_t,指向子线程的 TCB 地址 |
| 内核态(LWP) | pid_t(整数类型,本质是内核轻量级进程编号)等于当前进程的 PID,内核全局唯一 | pid_t(整数类型,本质是内核轻量级进程编号)内核分配的独立pid_t类型整数(≠进程 PID),内核全局唯一 |