一、多线程基础:别再把线程和进程混淆了!
【是什么】
线程(LWP,轻量级进程)就是进程的“打工人”—— 进程是“公司”(资源分配的基本单位,拥有独立内存),线程是“公司里的员工”(调度的最小单位,共享公司资源)。一个进程至少有一个“老板线程”(主线程),可以雇佣多个“员工线程”一起工作,员工之间共享办公室(进程的内存、文件句柄等),无需单独租赁场地(独立内存)。【为什么】
为何选择多线程而非多进程?主要是为了“节省成本”: 线程切换开销比进程小10倍以上:员工换岗(线程切换)无需更换办公室,而进程切换相当于更换公司,手续繁多; 共享资源便捷:员工之间传递文件(数据)可以直接放在办公桌上(共享内存),无需通过快递(IPC); 适合多任务并行:例如一个线程“接听电话”(处理网络请求),另一个线程“编写文档”(处理数据),互不影响。【怎么用】(基础概念无需编写代码,但需牢记)
核心记住3句话: 线程共享进程的资源(栈、堆、静态区除外,每个线程有自己的栈); 主线程结束即意味着整个公司的解散,所有员工(线程)也随之结束; 线程执行顺序随机(采用时间片轮转),谁先抢占CPU谁先执行,不可期望按创建顺序执行。【坑在哪】
线程不安全:多个员工争用同一台打印机(共享资源),若不排队将导致打印混乱(数据竞争); 一个线程崩溃可能影响整个进程:某员工翻桌(线程访问非法内存),可能导致整个公司(进程)崩溃; 不要以为线程越多越快:线程过多会导致频繁切换(上下文切换开销),反而变慢,这称为“线程爆炸”,类似于公司招聘过多员工导致内部消耗。 关键词:线程、进程、共享资源、调度、并行二、多线程编程(重点):这些函数让你少走99%的弯路
2.1 线程创建:pthread_create—— 雇佣员工工作
【是什么】
pthread_create
是“招聘员工”的核心函数,专门用于创建子线程(员工),使其执行指定的“工作任务”(线程函数)。
【为什么】
总不能让老板(主线程)独自完成所有任务吧?例如你编写一个下载软件,主线程负责界面,子线程负责下载,否则界面会卡死,用户可能认为程序崩溃,直接卸载。【怎么用】(3种场景,代码可以直接运行,不要随意修改!)
场景1:无参线程(员工执行固定任务,无需额外信息) cpp 运行#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 线程函数:员工的“工作内容”(必须是void*返回值、void*参数)
void* worker(void* arg) {
while (1) {
printf("分支线程:摸鱼中...(线程号:%#lx)\n", pthread_self());
sleep(1); // 摸鱼1秒,别让CPU炸了
}
}
int main() {
pthread_t tid; // 员工工号(线程号)
// 招聘员工:参数依次是(工号地址、线程属性(NULL=默认)、工作内容、参数(NULL=无参))
if (pthread_create(&tid, NULL, worker, NULL) != 0) {
printf("线程创建失败!你这招聘流程有问题啊~\n");
return -1;
}
printf("主线程:招聘成功!员工工号:%#lx\n", tid);
// 老板自己也得干活,不然主线程结束,员工直接失业
while (1) {
printf("主线程:监督摸鱼...\n");
sleep(1);
}
return 0;
}
编译命令(忘记添加-lpthread将会报错):
g++ -o no_arg_thread no_arg_thread.cpp -lpthread
场景2:传递单个数据(给员工分配任务目标)
cpp
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* worker(void* arg) {
// void*转具体类型(别瞎转!转错直接段错误,哭都来不及)
int task_num = *(int*)arg;
printf("分支线程:收到任务!要干%d件活\n", task_num);
pthread_exit(NULL); // 干完活下班,别赖着
}
int main() {
pthread_t tid;
int num = 5; // 任务指标:干5件活
// 传参:把num的地址传进去(注意:别传局部变量的临时值!)
if (pthread_create(&tid, NULL, worker, &num) != 0) {
printf("线程创建失败,滚去查编译命令!\n");
return -1;
}
pthread_join(tid, NULL); // 等员工干完活再下班
printf("主线程:任务完成,发工资!\n");
return 0;
}
场景3:传递多个数据(给员工分配完整任务包:编号、名称、工作量)
cpp
运行
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
// 任务包(结构体是多线程传多个数据的标准答案,别用全局变量,显得你很菜)
struct Task {
int id; // 任务编号
char name[20]; // 任务名字
int workload; // 工作量
};
void* worker(void* arg) {
// 结构体指针转换(记住这个套路,面试常考!)
struct Task* task = (struct Task*)arg;
printf("分支线程:任务ID:%d,任务名:%s,工作量:%d\n",
task->id, task->name, task->workload);
pthread_exit(NULL);
}
int main() {
pthread_t tid;
struct Task task = {1001, "写BUG", 3}; // 任务:写3个BUG(程序员的日常)
if (pthread_create(&tid, NULL, worker, &task) != 0) {
printf("创建失败?建议检查是不是没装pthread库~\n");
return -1;
}
pthread_join(tid, NULL);
printf("主线程:BUG写得好,扣绩效!\n");
return 0;
}
【坑在哪】
编译时忘记添加-lpthread
:会报“未定义引用”的错误,纯属自找麻烦;
线程函数签名不正确:必须是
void* (*start_routine)(void*)
,返回值和参数都不能错;
传递局部变量地址:主线程完成后变量被销毁,子线程访问野指针,程序直接崩溃;
主线程提前退出:员工仍在工作,老板却走了,进程直接终止,线程的努力白费。
关键词:pthread_create、线程函数、void * 传参、-lpthread、结构体传参
2.2 线程号:pthread_self—— 查询员工工号
【是什么】
pthread_self()
是“员工查询自己工号”的函数,返回当前线程的ID,无需传递参数,永远成功(比你上班打卡还可靠)。
【为什么】
用于调试!多个线程同时运行,你需要知道哪个是哪个?打印线程号就能区分“谁在偷懒,谁在工作”。【怎么用】
cpp 运行#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* worker(void* arg) {
// 线程自己查工号,比主线程传过来靠谱
printf("分支线程:我的工号是%#lx\n", pthread_self());
pthread_exit(NULL);
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
printf("主线程:我查的员工工号是%#lx\n", tid); // 主线程记录的工号
pthread_join(tid, NULL);
return 0;
}
【坑在哪】
别将pthread_t
当作int类型打印:线程号是无符号长整型,使用
%#lx
格式,否则会打印一堆乱码,误以为程序出问题。
关键词:pthread_self、线程ID、%#lx
2.3 线程退出:pthread_exit—— 员工下班
【是什么】
pthread_exit(void* retval)
是“员工主动下班”的函数,参数是退出状态(通常填NULL,没有人关心你是如何下班的)。
【为什么】
线程完成任务后应主动退出,不要占用进程资源!例如下载线程下载完文件后直接退出,比让主线程催促更优雅。【怎么用】
cpp 运行#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* worker(void* arg) {
printf("分支线程:干活ing...\n");
sleep(2);
pthread_exit(NULL); // 干完活下班,不拖沓
// 下面的代码永远执行不到,别写废话
printf("我还没走!");
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_join(tid, NULL);
printf("主线程:员工下班,我也溜了~\n");
return 0;
}
【坑在哪】
不要在主线程中使用pthread_exit
:主线程退出,整个进程也会结束,其他线程无论是否完成任务都会随之终止,相当于老板逃跑,员工集体失业。
关键词:pthread_exit、线程退出、NULL
2.4 线程回收:pthread_join—— 老板等待员工下班
【是什么】
pthread_join(pthread_t thread, void** retval)
是“老板等待员工下班”的函数,阻塞主线程,直到指定线程退出,还可以回收线程资源(避免僵尸线程)。
【为什么】
你招聘员工工作,不等他完成就关门大吉?未回收的线程会变成“僵尸线程”,占用系统资源却不工作,就像职场上的老油条一样令人讨厌。【怎么用】
cpp 运行#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* worker(void* arg) {
printf("分支线程:加班ing...\n");
sleep(3);
printf("分支线程:下班!\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
printf("主线程:等员工加班完...\n");
pthread_join(tid, NULL); // 阻塞等,员工不下班我不走
printf("主线程:员工下班,我也下班!\n");
return 0;
}
【坑在哪】
忽略pthread_join主线程过早退出,线程资源未释放,僵尸线程累积,系统资源被占用;
回收已分离的线程:线程设为分离状态后,不应使用
pthread_join
回收,否则会出错,纯属多余。
关键词:pthread_join、阻塞回收、僵尸线程、资源回收
2.5 线程分离态:pthread_detach—— 员工自动结算????
【是什么】
pthread_detach(pthread_t thread)
是 “员工离职后自动结算” 的函数,将线程设置为分离状态,退出后系统自动回收资源,无需主线程
pthread_join
监控。
【为什么】
有些线程完成任务后不再有用,无需主线程等待回收,例如日志线程,写完日志直接退出,系统自动清理,避免主线程操心。
【怎么用】
cpp
运行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* worker(void* arg) {
printf("分支线程:分离态干活,干完自动溜~\n");
sleep(2);
printf("分支线程:溜了溜了~\n");
pthread_exit(NULL);
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_detach(tid); // 设为分离态,不用等它下班
printf("主线程:不管员工了,我先摸鱼...\n");
sleep(3); // 给员工足够时间干活,别主线程先溜了
printf("主线程:我也溜了~\n");
return 0;
}
【坑在哪】??
分离状态的线程无法回收:设为分离状态后,再使用
pthread_join
会出错,别自找麻烦;
主线程提前退出:分离状态的线程仍在运行,主线程提前退出,线程仍会被终止,徒劳无功。
关键词:pthread_detach、分离状态、自动回收、非阻塞
【毒舌总结】
多线程基础就这么点内容,记住:
线程是进程的 “工作者”,共享资源但不安全;
核心函数只有 5 个:
pthread_create
(招聘)、
pthread_self
(查询工号)、
pthread_exit
(下班)、
pthread_join
(等待下班)、
pthread_detach
(自动结算);
编译必须加上
-lpthread
,传递参数时不要随意转换类型,回收时不要遗漏,分离状态不要滥用;
不要一开始就搞同步互斥,先掌握 “线程创建 - 传参 - 回收”,否则同步互斥只会让你死得更惨~
复习时按关键词查找,代码直接修改即可使用,再出错你就该反思自己是否不适合编程(bushi)~ 下次学习同步互斥,我会详细吐槽那些让你崩溃的竞争条件和死锁!
多线程编程复习查找表(毒舌速查版)
(按 “关键词搜索→瞬间理解核心” 设计,找不到算表格设计差,记不住算你懒~)
| 模块 | 关键词 | 核心内容(人话总结) | 关键代码片段(精简版) | 坑点 & 注意事项 ?? | ||
|---|---|---|---|---|---|---|
| 多线程基础 | 线程 vs 进程 | 进程 = 公司(资源分配单位),线程 = 员工(调度单位);线程共享进程资源,切换开销小,不安全。 | - | 1. 线程崩溃可能导致整个进程崩溃; 2. 多线程争夺资源必定混乱(竞争); 3. 主线程退出 = 全部线程结束。 |
||
| 多线程基础 | 线程执行顺序 | 随机!按 CPU 时间片轮询,谁抢到谁执行,不要期望按创建顺序来。 | - | 不要编写 “线程 1 先执行,线程 2 后执行” 的逻辑,纯粹是自找麻烦。 | ||
| 多线程编程 - 创建 | pthread_create、-lpthread | 雇佣员工干活,参数:(工号地址,默认属性 NULL,工作函数,传参);编译必须加 - lpthread。 | |
1. 函数签名必须是 | |
;2. 传递局部变量地址 = 野指针;3. 忘记加 - lpthread 必报错。 |
| 多线程编程 - 线程号 | pthread_self、%#lx | 线程查询自己的工号,返回无符号长整型,打印用 %#lx。 | |
不要用 % d 打印!线程号是长整型,错误打印会导致垃圾值。 | ||
| 多线程编程 - 退出 | pthread_exit、NULL | 线程主动下班,参数填 NULL(没人关心退出状态);主线程不要使用,否则整个进程结束。 | |
退出后下面的代码永远执行不到,不要写多余的代码。 | ||
| 多线程编程 - 回收 | pthread_join、阻塞 | 老板等待员工下班,阻塞主线程直到线程退出,回收资源(防止僵尸线程)。 | |
1. 遗漏回收 = 僵尸线程累积; 2. 回收分离状态的线程 = 出错。 |
||
| 多线程编程 - 分离态 | pthread_detach、自动回收 | 设置为分离状态,线程退出后系统自动回收,无需 join;适用于 “完成即走” 的线程(如日志线程)。 | |
1. 分离后不能 join; 2. 主线程不要提前退出,否则线程白干。 |
||
| 多线程编程 - 传参 | 单数据、结构体传参 | 单数据传地址,多数据用结构体(不要用全局变量!);转换时必须强制转换对应类型。 | |
1. 传递局部变量 = 野指针; 2. 强制转换类型错误 = 段错误(哭都来不及)。 |


雷达卡


京公网安备 11010802022788号







