文章目录
- [1. 线程概念](#1. 线程概念)
- [2. 线程控制](#2. 线程控制)
1. 线程概念
什么是线程?
举个"住房"的例子理解:
- 传统的进程(单线程):像一个人独居
- 他拥有整个房子(虚拟地址空间)
- 客厅、厨房、厕所(代码段、数据区、堆区)都是他一个人的
- 他做饭、扫地、洗衣服(CPU调度执行)一件事一件事做
- 多线程进程:多人合租
- 资源共享: 大家住在这个房子里(共享同一个地址空间、页表、文件描述符 )。有人在客厅放个杯子(全局变量),其他人都能看见并修改
- 独立调度: 每个租客都是独立的个体,A在看电视(线程A),B在做饭(线程B),互不干扰
- 私有空间: 大家共用房子,每个人有自己的房间(栈空间 )和记事本(寄存器上下文)-->记住自己刚才干到哪一步了
一个进程创建伴随着进程控制块(PCB)、进程地址空间(mm_struct)以及页表的创建

每个进程都有自己独立的虚拟地址空间和独立的页表,意味着进程在运行时具有独立性
站在Linux内核角度理解:到底有没有线程?
Linux中没有真正意义上的线程
Windows VS Linux:
- Windows:内核中真正定义了进程描述符和线程描述符两个数据结构
- Linux:内核设计者觉得不需要这么复杂,线程本质就是共享了资源的进程,那么可以复用大量已经实现的代码
Linux的实现方式:轻量级进程(LWP)
Linux内核眼里,CPU只看到了task_struct(PCB)
- 创建普通进程(fork):
- 复制一个
task_struct - 复制一张全新的虚拟地址空间(页表指向新的物理内存--->这种情况还是发生数据修改写时拷贝才做的)
- 关系:独立、各过各的、老死不相往来
- 复制一个
- 创建线程(
clone/pthread_create):- 复制一个**
task_struct** - 但是!不复制虚拟地址空间,而是让新的**
task_struct指针 指向和旧的一样的mm_struct!** - 关系:肉体(PCB)独立,灵魂(内存视角)共享
- 复制一个**

我们创建了3个线程,可以总结出:
- 每个线程都是当前进程里的一个执行流
- 线程在进程内部运行,本质是线程在进程地址空间内运行,进程申请的资源,几乎被所有线程共享
重新定义进程和线程
- 进程是分配系统资源的基本单位--->一间房子,属于一栋楼系统的一份资源实体
- 线程是系统进行CPU调度的基本单位--->一间房子里的每个租客

红框整个整体是进程,当然进程还有文件、信号等其他控制块---->我们之前的定义没有错:进程=内核数据结构+代码数据
总结:谈一下Linux下的进程和线程区别
从用户视角来看,线程是进程内部的执行流
但从Linux内核视角来看,其实并没有严格区分进程和线程。Linux 将它们统一实现为 task_struct ,所以CPU调度时,不关心当前调度的task_struct是进程还是线程,只关心一个一个的独立执行流
所谓的线程,在Linux内核中被称为轻量级进程 (LWP) 。它们的本质区别在于 clone 时传入的标志位不同
- 如果是普通进程,拥有独立的虚拟地址空间 (
mm_struct) - 如果是线程,多个
task_struct会指向同一个mm_struct,共享代码段、数据段和堆
所以,在Linux下,CPU调度时只看 task_struct,看到的都是一个个独立的执行流,这使得Linux的线程调度非常高效,不需要维护复杂的层级关系
2. 线程控制
创建线程
代码
cpp
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
// 结构体传参
struct ThreadData{
int thread_id;
std::string msg;
};
std::string ToHex(pthread_t tid){
char buf[64];
snprintf(buf, sizeof(buf), "0x%lx", tid);
return buf;
}
// 新线程入口函数
void* thread_routine(void* arg){
// 参数是任意类型 按照我们需要传递的类型强转
ThreadData* data = static_cast<ThreadData*>(arg);
// 获取当前线程的ID
pthread_t tid = pthread_self();
std::cout << "新线程启动!库ID:" << ToHex(tid)
<<" | 业务ID:" << data->thread_id
<<" | 消息:" << data->msg << std::endl;
sleep(3);
std::cout << "新线程任务完成,准备退出...\n";
std::string* result = new std::string("success");
return (void*)result;
}
int main(){
std::cout << "主线程开始运行, pid: " << getpid() << std::endl;
pthread_t tid; // 新线程ID
// C++11列表初始化
ThreadData* tdata = new ThreadData { 1, "hello from thread!"};
// 参数1 线程ID指针->输出型参数
// 参数2 线程属性->nullptr是默认属性
// 参数3 线程函数->函数指针
// 参数4 传给线程函数的参数->任意类型
int ret = pthread_create(&tid, nullptr, thread_routine, (void*)tdata);
if(ret != 0){
std::cerr << "线程创建失败,错误码:" << ret <<std::endl;
}
std::cout << "主线程创建新线程成功,新线程ID: " << tid << std::endl;
std::cout << "主线程做我的事情...\n";
void* thread_result;
std::cout << "主线程阻塞等待新线程结束...\n";
pthread_join(tid, &thread_result);
std::string* res_str = static_cast<std::string*>(thread_result);
std::cout << "主线程等待新线程退出成功,返回值:" << *res_str << std::endl;
delete tdata;
delete res_str;
return 0;
}
函数接口
cpp
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 参数:
thread:线程返回IDattr:线程属性,nullptr表示默认属性start_routine:线程启动入口函数,函数指针arg:传给入口函数的参数,可以是任意类型- 返回值:
- 成功返回0
- 失败返回错误码
参数4为什么传void*?
为了通用性,C语言没有模板,OS不知道要传什么类型,使用
void*表示接收任何类型的参数注意: 一定不能传栈变量的地址 ,如果主线程比子线程先结束,或者主线程进入了下一个循环,栈里的变量销毁了,子线程读到的就是垃圾数据,一定要用
new在堆上开辟数据,或者确认主线程生命周期长于子线程
使用指令查看线程信息

- LWP得到的是真正的线程ID
- 在
ps -aL得到线程ID后,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在栈区,其他线程的栈在共享区,因为pthread系列函数是pthread库提供的,动态库加载在共享区
怎么解释pthread_self()打印出来的ID?
- 打印出来的是一个很大的数字->地址,这个ID是用户态线程库维护的ID,本质是该线程在库中元数据(线程ID、线程栈、寄存器)的内存首地址
- 想要获取内核态的LWP ID需要
gettid()
原理剖析
原理一:Linux里没有真正的线程
在Linux内核看来,线程就是进程,并没有为线程专门定义一个struct thread,依然使用struct task_struct
调用pthread_create时,底层最终调用的时clone()系统调用:
cpp
// 伪代码演示 clone 的调用
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);
CLONE_VM(Virtual Memory):
- 含义 :新老进程共享内存描述符 (
mm_struct)。- 效果 :这就是为什么在主线程
new了一个对象,子线程能直接通过指针访问!因为它们的**页表(Page Table)**指向的是同一块物理内存
CLONE_FILES:
- 含义:共享文件描述符表。
- 效果 :主线程打开了
log.txt,子线程直接能写,不需要重新 open
原理二:线程独有资源VS共享资源
| 共享资源 (体现轻量级) | 独有资源 (体现独立调度) |
|---|---|
| 地址空间 (代码段、数据段、堆) | 栈 (Stack):每个线程必须有独立栈来保存自己的局部变量和函数调用链 |
| 文件描述符表 (打开的文件) | 寄存器上下文 (PC指针、SP指针):线程切换的核心就是切寄存器 |
| 信号处理方式 (Handler) | 线程 ID (LWP) |
| 当前工作目录 | 信号屏蔽字 (Signal Mask) |
原理三:局部性原理的应用
线程共享同一块虚拟地址空间,CPU在切换线程时,不需要切换页表(CR3寄存器不变),这意味着TLB中的缓存依然有效,Cache命中率极高
销毁线程
代码
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
using namespace std;
// 方式一 自然死亡
void* thread_return(void* arg){
cout << "线程1 正在运行,准备通过return退出...\n";
sleep(1);
return (void*)10;
}
// 方式二 自杀 pthread_exit
void* thread_exit_self(void* arg){
cout << "线程2 正在运行,准备通过phread_exit退出...\n";
sleep(1);
sleep(3);
// 立即终止当前线程 后续代码不会执行
pthread_exit((void*)20);
cout << "线程2 这行代码不会被执行!\n";
return nullptr;
}
// 方式三 他杀 pthread_cancel
void* thread_cancel(void* arg){
cout << "线程3 正在死循环运行,等待被主线程取消...\n";
while(1){
// 这里的sleep是个取消点 只有遇到取消点 线程才会响应cancel信号
sleep(1);
cout << "线程3 还活着...\n";
}
return nullptr;
}
// 致命陷阱
void* thread_trap(void* arg){
cout << "陷阱线程 如果我调用exit(1) 整个进程都会挂掉!\n";
// exit(1); // 取消注释 会发现整个程序直接退出 后面的log全没了
return nullptr;
}
int main(){
pthread_t tid1, tid2, tid3, tid4;
void* ret_val;
pthread_create(&tid1, nullptr, thread_return, nullptr);
pthread_create(&tid2, nullptr, thread_exit_self, nullptr);
pthread_create(&tid3, nullptr, thread_cancel, nullptr);
// pthread_create(&tid4, nullptr, thread_trap, nullptr);
cout << "==============================================" << endl;
pthread_join(tid1,&ret_val);
cout << "主线程 等待线程1退出,返回值:" << (long long)ret_val << endl;
pthread_join(tid2,&ret_val);
cout << "主线程 等待线程2退出,返回值:" << (long long)ret_val << endl;
cout << "主线程 请求取消线程3...\n";
pthread_cancel(tid3);
pthread_join(tid3,&ret_val);
// 如果线程是被 cancel 的
// join 得到的返回值是一个宏 PTHREAD_CANCELED (通常是 -1)
if(ret_val == PTHREAD_CANCELED)
cout << "主线程 线程3成功被取消...\n";
else
cout << "主线程 线程3异常退出...\n";
cout << "==============================================" << endl;
cout << "主线程 所有演示结束\n";
return 0;
}
函数接口
pthread_exit
cpp
#include <pthread.h>
void pthread_exit(void *retval);
- 功能:终止调用该函数的线程
- 细节:
- 释放线程独有的资源,但不会释放进程共享的资源
在main函数中调用pthread_exit会发生什么?
- 直觉:
main结束,进程就结束 - 事实:如果在
main结尾调用pthread_exit而不是return或exit,主线程会终止,但只要有其他子线程在运行,进程依然活着
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 子线程函数:它要活得比主线程长
void* worker(void* arg) {
for (int i = 1; i <= 5; i++) {
sleep(1);
cout << "[子线程] 我还在运行... 第 " << i << " 秒" << endl;
}
cout << "[子线程] 任务完成,我也要退出了。此时进程才会真正结束。" << endl;
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
cout << "[主线程] 我只负责启动子线程,现在我要先走了!" << endl;
// 重点来了!!!
// 如果这里写 return 0; 或者 exit(0); 子线程会瞬间被杀掉。
// 但是我们用 pthread_exit,只结束主线程自己。
pthread_exit(nullptr);
// 这行代码永远不会执行
cout << "这行代码绝对看不见!" << endl;
return 0;
}
输出:
txt
[主线程] 我只负责启动子线程,现在我要先走了!
[子线程] 我还在运行... 第 1 秒
[子线程] 我还在运行... 第 2 秒
[子线程] 我还在运行... 第 3 秒
[子线程] 我还在运行... 第 4 秒
[子线程] 我还在运行... 第 5 秒
[子线程] 任务完成,我也要退出了。此时进程才会真正结束。
原理剖析:
- 原理一:return/exit VS pthread_exit的区别
return 0(main中)/exit:
- 调用系统调用
exit_group()- 含义:把这个进程组里的所有人全都干掉,回收内存,关闭文件
pthread_exit():
- 调用系统调用
exit()- 含义:只把当前这个调度实体的状态设为死亡,不要动共享资源(内存、文件描述符)
- 原理二:进程退出的判断标准
- 隶属于这个进程组的最后一个线程退出时,进程退出
- Linux内核维护了一个计数器:
- 程序启动,
cnt = 1->主线程pthread_create,cnt = 2->主+子- 主线程调用
pthread_exit,cnt--->cnt = 1
- **此时
cnt不为0!内核认为进程依然活着!mm_struct**依然保留!- 子线程运行结束,
cnt = 0
- 内核检测到归零,彻底回收进程资源
pthread_cancel
cpp
#include <pthread.h>
int pthread_cancel(pthread_t thread);
- 功能:向指定线程发送终止请求
- 底层细节(取消点)
- 这是一个异步请求,调用了cancel,对方不一定马上死
- 目标线程必须运行到某个系统调用 (如
sleep、read、write)时,检测到取消信号,才退出 - 不推荐使用
return VS exit VS pthread_exit
| 函数 | 作用范围 | 影响 |
|---|---|---|
| return (在线程函数中) | 仅终止当前线程 | 最安全、最推荐的方式 |
| pthread_exit | 仅终止当前线程 | 可以在函数深层嵌套中直接退出线程,无需层层 return |
| exit() | 终止整个进程 | 核武器 。任何一个线程调用 exit,所有线程(包括主线程)全部瞬间蒸发,内存全部回收 |
线程是怎么死的?
资源的释放:
- 当线程终止时,内核会将该线程的状态设置为
EXIT_ZOMBIE(僵尸状态) - 它的栈和
task_struct结构体依然保留 - 为什么要保留?因为要存返回值! 等着别人来取(Join)
pthread_join 的本质:
pthread_join本质上是在等待目标线程的task_struct变成EXIT_ZOMBIE- 一旦等到,主线程会把该线程的返回值(存储在
task_struct的某个字段里)拷贝出来,然后通知内核:"你可以把这个线程彻底回收了(释放 task_struct)" - 如果不 join,这块
task_struct内存就泄露了(内存泄漏)
等待线程
代码
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
using namespace std;
// 场景1:子线程计算数据 返回一个结构体指针
struct Result{
int code;
string msg;
};
void* thread_work(void* arg){
long long id = (long long)arg;
cout << "子线程 " << id << " 开始工作...\n";
sleep(2);
// 返回值必须在堆区开辟!!!或者使用静态/全局变量
// 绝对不能返回局部变量地址!!!函数结束栈帧就销毁了
Result* res = new Result();
res->code = 200;
res->msg = "success";
cout << "子线程 " << id << " 工作完成,返回结果地址:" << res << endl;
return (void*)res;
}
// 场景2:子线程被异常取消
void* thread_bad(void* arg){
cout << "子线程bad 我来打酱油,马上被cancel...\n";
while(1) sleep(1);
return nullptr;
}
int main(){
pthread_t tid_work, tid_bad;
pthread_create(&tid_work, nullptr, thread_work, nullptr);
cout << "主线程 正在阻塞等待子线程完成...\n";
void* ret_ptr = nullptr;
int n = pthread_join(tid_work, &ret_ptr);
if(n == 0){
Result* final_res = (Result*)ret_ptr;
cout << "主线程 成功回收工作线程!\n";
cout << " |--返回地址: " << ret_ptr << endl;
cout << " |--状态码: " << final_res->code << endl;
cout << " |--消息: " << final_res->msg << endl;
delete final_res;
}
cout << "------------------------------------------" << endl;
pthread_cancel(tid_bad);
pthread_join(tid_bad, &ret_ptr);
if(ret_ptr == PTHREAD_CANCELED)
cout << "主线程 检测到子线程时被强制取消的!\n";
return 0;
}
函数接口
cpp
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
-
功能:以阻塞等待的方式等待指定线程结束
-
参数一:要等的哪个线程ID
-
参数二
- 类型
void**指向void指针的指针 - 作用:存储子线程的返回值
- 类型
-
为什么要二级指针?
- 子线程的函数定义是
void* func(void* arg),返回一个void*(比如指向堆内存的地址0x1234) - 主线程想要拿到这个地址
0x1234
- 如果只传
void*,同样都是一级指针,值传递,函数内部修改不了外部变量的值 - 举个例子:想在函数内部修改一个
int,需要传int* - 推导:而现在想要在函数内部修改一个
void*,需要传void**
- 子线程的函数定义是
线程是如何阻塞和唤醒的?
- 调用join
- 主线程调用
pthread_join - 内核检查目标线程
tid的状态 - 如果目标线程还在运行,主线程将自己的状态从
TASK_RUNNING改为TASK_INTERRUPTIBLE(可中断睡眠状态) - 主线程被放入一个等待队列 (Wait Queue) ,移出 CPU 运行队列。此时主线程卡死,不占用 CPU
- 主线程调用
- 子线程结束
- 子线程运行完毕,调用
pthread_exit - 内核讲子线程状态设为
EXIT_ZOMBIE - 内核向等待队列发信号,唤醒主线程
- 子线程运行完毕,调用
- 资源回收
- 主线程醒来,恢复
TASK_RUNNING - 主线程读取子线程
task_struct中保存的退出码->void*返回值,填入void** retal里 - 内核释放子线程的
task_struct和内核栈,子线程彻底消失
- 主线程醒来,恢复