Linux 多线程编程------总结
一句话总结全文:线程是进程内部的"流水线工人"------共享厂房(内存)、各自干活(独立栈),需要沟通(线程通讯)和协调(互斥锁+条件变量)才能高效有序地完成生产任务。
一、线程的概念
一句话总结:线程是进程里的"执行流水线"------一个进程至少有一根流水线,也可以有多根流水线同时干活。
1.1 什么是线程?
线程(Thread) 是进程中的一个执行单元,负责当前进程中程序的执行。一个进程中 至少有一个线程(称为主线程),也可以有多个线程。
进程 = 内存资源 + 主线程 + 子线程 + 子线程 + ...
1.2 生活类比:公司大楼 vs 员工
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 计算机概念 │ 生活类比 │ 关键特征 │
├──────────────┼────────────────────────────────┼─────────┤
│ 进程 │ 一栋独立的公司大楼 │ 独门独户 │
│ 线程 │ 大楼里的员工 │ 共享资源 │
│ 多进程 │ 多栋独立的大楼 │ 各自独立 │
│ 多线程 │ 一栋楼里多个员工协作 │ 沟通方便 │
│ 进程间通讯 │ 跨公司发公函(慢、复杂) │ 需要中介 │
│ 线程间通讯 │ 同事间打内部电话(快,但需协调) │ 需同步机制 │
└──────────────┴────────────────────────────────┴─────────┘
展开故事:想象一家公司(进程)租了一栋办公楼(内存空间)。公司里有总经理(主线程)、财务(子线程1)、销售(子线程2)、行政(子线程3)。他们:
- 共享:办公室、打印机(文件描述符)、应急预案(信号处理方式,一处改动全员响应)、公司地址(工作目录)
- 独立:每个人有自己的工位(线程栈)、工牌(线程ID)、记事本(寄存器值)、备忘录(errno)
1.3 进程 vs 线程 简笔画图
进程A(独立公司) 进程B(独立公司)
┌──────────────────────────────┐ ┌──────────────────────────┐
│ ┌──────┬──────┬──────┐ │ │ ┌──────┬──────┐ │
│ │总经理│财务 │销售 │ │ │ │总经理│运营 │ │
│ │(主线)│(子线)│(子线)│ │ │ │(主线)│(子线)│ │
│ └──────┴──────┴──────┘ │ │ └──────┴──────┘ │
│ ┌────────────────────┐ │ │ ┌────────────────┐ │
│ │ 共享区域: │ │ │ │ 共享区域: │ │
│ │ - 办公室(内存) │ │ │ │ - 办公室 │ │
│ │ - 打印机(文件) │ │ │ │ - 打印机 │ │
│ │ - 应急预案(信号) │ │ │ │ - 应急预案 │ │
│ └────────────────────┘ │ │ └────────────────┘ │
└──────────────────────────────┘ └──────────────────────────┘
↑ 线程间:打内部电话(需协调) ↑ 进程间:发公函(IPC)
1.4 线程共享 vs 独立资源
┌─────────────────────────────────────────────────────┐
│ 进程(一栋公司大楼) │
│ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ 线程1 │ │ 线程2 │ │ 线程3 │ │
│ │ 总经理 │ │ 财务 │ │ 销售 │ │
│ │ (独立) │ │ (独立) │ │ (独立) │ │
│ │•工位栈 │ │•工位栈 │ │•工位栈 │ │
│ │•工牌ID │ │•工牌ID │ │•工牌ID │ │
│ │•记事本 │ │•记事本 │ │•记事本 │ │
│ └───┬───┘ └───┬───┘ └───┬───┘ │
│ └──────────┼──────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 共享区域(整栋楼共享) │ │
│ │ • 操作手册(代码段 .text,全员共用) │ │
│ │ • 公告板(数据段 .data/.bss 全局变量) │ │
│ │ • 共享材料库(堆 heap,登记领取) │ │
│ │ • 门禁卡(文件描述符表) │ │
│ │ • 公司地址(当前工作目录) │ │
│ │ • 火灾应急预案(信号处理,一处触发全员响应) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
1.5 进程 vs 线程对比表
| 对比维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 一句话定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立(隔离,互不干扰) | 共享(同一进程内) |
| 创建开销 | 大(需分配独立地址空间) | 小(复用进程资源) |
| 切换速度 | 慢(需切换地址空间) | 快(同一地址空间内切换) |
| 通讯方式 | 复杂(管道/消息队列/共享内存) | 简单(直接读写全局变量) |
| 相同点 | 都有独立执行流、都有ID、都可被调度 | --- |
| 不同点 | 资源隔离、切换慢、通讯复杂 | 资源共享、切换快、通讯简单 |
| 注意点 | 进程间通讯需注意死锁 | 多线程需注意竞态条件 |
| 适用场景 | 需要高隔离、高可靠性的任务 | 需要高并发、低延迟、数据共享的任务 |
二、线程相关命令
一句话总结 :用
pidstat、top、ps三个命令,像查户口一样查清进程内部有多少线程在干活。
2.1 生活类比:工厂车间巡查
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 命令 │ 生活类比 │ 特点 │
├──────────────┼────────────────────────────────┼─────────┤
│ pidstat -t │ 拿出工牌号查每个人的工作统计报告 │ 历史数据 │
│ top -H │ 站在高处俯瞰,实时看谁在忙 │ 实时刷新 │
│ ps -T │ 翻花名册,看谁在哪个车间 │ 快照清点 │
└──────────────┴────────────────────────────────┴─────────┘
2.2 三个命令关系图
进程 PID=153729(有3个线程)
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ pidstat │ │ top │ │ ps │
│ 查统计 │ │ 实时看 │ │ 快照 │
├──────────┤ ├──────────┤ ├──────────┤
│TGID TID │ │ PID CPU │ │ PID SPID │
│153729 │ │153729 │ │153729 │
│ │_153730│ │ │_153730│ │ │_153730│
│ │_153731│ │ │_153731│ │ │_153731│
└──────────┘ └──────────┘ └──────────┘
2.3 pidstat 命令
bash
# 安装
sudo apt install sysstat
# 查看 PID=153729 的进程下所有线程的 CPU 占用
pidstat -t -p 153729
执行结果:
02:35:01 PM UID TGID TID %CPU CPU Command
02:35:01 PM 1000 153729 153729 0.01 0 a.out ← 主线程
02:35:01 PM 1000 153729 153730 0.05 1 |__a.out ← 子线程1
02:35:01 PM 1000 153729 153731 0.03 2 |__a.out ← 子线程2
解读 :TGID = 进程ID,TID = 线程ID。主线程 TID = TGID。
2.4 top 命令
bash
top -H -p 153729
执行结果:
Threads: 3 total
PID USER %CPU COMMAND
153729 user 0.0 a.out
153730 user 0.3 |__a.out
153731 user 0.3 |__a.out
2.5 ps 命令
bash
ps -T -p 153729
执行结果:
PID SPID CMD
153729 153729 a.out
153729 153730 a.out
153729 153731 a.out
2.6 三个命令对比表
| 命令 | 特点 | 最佳场景 | 关键选项 | 相同点 | 注意点 |
|---|---|---|---|---|---|
pidstat -t |
历史累积统计 | 性能监控、数据采集 | -t -p PID |
都查线程 | 需先安装 sysstat |
top -H |
实时动态刷新 | 现场排查 CPU 飙高 | -H -p PID |
都查线程 | 交互式,按 q 退出 |
ps -T |
一次性快照 | 快速查看线程列表 | -T -p PID |
都查线程 | 输出最简洁 |
三、线程创建
一句话总结 :用
pthread_create函数给进程"招一个临时工"------指定它干什么活,招聘成功后它就开始独立干活。
3.1 生活类比:餐厅招聘
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 计算机概念 │ 生活类比 │ 说明 │
├──────────────┼────────────────────────────────┼─────────┤
│ pthread_creat│ 老板发招聘通知 │ 创建线程 │
│ 主线程 │ 老板(餐厅经理) │ 管理者 │
│ 子线程 │ 新招的厨师 │ 干活的人 │
│ 线程函数 │ 厨师的工作任务(炒菜) │ 做什么 │
│ 函数参数 │ 厨师要用的食材清单 │ 传什么 │
│ pthread_join │ 老板等厨师做完菜再下班 │ 等待回收 │
│ 主线程提前退出 │ 老板提前关门,厨师被迫停工 │ 经典错误 │
└──────────────┴────────────────────────────────┴─────────┘
3.2 线程创建流程
主线程(老板) 子线程(厨师)
│ │
│ 1. 准备食材(准备参数) │
│ │
│ 2. pthread_create(招聘) │
│─────────────────────────────>│
│ 3. 返回 0(招聘成功) │
│ │
│ 4. 继续做自己的事 │ 5. 开始炒菜(执行函数)
│ (并发执行) │ (并发执行)
│ │
│ 6. pthread_join(等厨师做完) │
│<─────────────────────────────│
│ │ 7. 厨师做完(线程退出)
│ 8. 一起下班 │
│ │
3.3 经典示例:餐厅点餐
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 厨师的菜单结构体
typedef struct {
char name[64];
int table_id; // 桌号
} Order;
// 厨师的工作:根据订单做菜
void *cook(void *arg)
{
Order *order = (Order *)arg;
printf("【厨师】开始做: %s(送往 %d 号桌)\n",
order->name, order->table_id);
return NULL;
}
int main(void)
{
pthread_t chef_tid;
Order order = {"鱼香肉丝", 5}; // 订单:5号桌的鱼香肉丝
// 老板把订单交给厨师,让他开始做
pthread_create(&chef_tid, NULL, cook, &order);
printf("【老板】厨师开始做了,我继续招呼客人...\n");
// 老板等厨师做完再下班(关键!)
pthread_join(chef_tid, NULL);
printf("【老板】菜做好了,打烊下班!\n");
return 0;
}
执行结果:
【老板】厨师开始做了,我继续招呼客人...
【厨师】开始做: 鱼香肉丝(送往 5 号桌)
【老板】菜做好了,打烊下班!
注意 :这里传递的是 main 函数栈上的局部变量
order的地址。由于主线程用pthread_join等待子线程结束,所以order在整个子线程执行期间都有效,不会出现野指针问题。
3.4 经典陷阱:老板提前关门
c
// 错误写法:没有 pthread_join
int main(void)
{
pthread_t chef_tid;
Order order = {"鱼香肉丝", 5};
pthread_create(&chef_tid, NULL, cook, &order);
printf("【老板】厨师开始做了...\n");
return 0; // ← 老板提前关门!厨师还没做完就被赶走了!
}
可能结果(只打印了老板的话,厨师的没打印):
【老板】厨师开始做了...
原因:主线程 return 0 导致整个进程结束,子线程刚被创建还没来得及执行就被强行终止了。
3.5 三种解决方案
| 方案 | 代码 | 原理 | 推荐度 |
|---|---|---|---|
pthread_join 等待 |
pthread_join(tid, NULL); |
阻塞等子线程结束 | 推荐 |
主线程 pthread_exit |
pthread_exit(NULL); |
主线程退出但不结束进程 | 次选 |
sleep 延迟 |
sleep(1); |
等一会(不可靠) | 不推荐 |
3.6 pthread_create 函数详解
c
#include <pthread.h>
int pthread_create(pthread_t *thread, // 输出:线程 ID
const pthread_attr_t *attr, // 线程属性(NULL=默认)
void *(*start_routine)(void *), // 线程执行函数
void *arg); // 传给执行函数的参数
| 参数 | 说明 |
|---|---|
thread |
指向 pthread_t 的指针,函数返回时保存新线程的 ID |
attr |
线程属性(栈大小、调度策略等),传 NULL 使用默认属性 |
start_routine |
线程入口函数,格式为 void *func(void *arg) |
arg |
传递给入口函数的参数,可以是任意类型指针 |
返回值 :成功返回 0 ,失败返回 错误码。
编译命令 :
gcc program.c -lpthread -o program
3.7 创建线程对比表
| 对比项 | pthread_create() |
fork() |
|---|---|---|
| 创建实体 | 线程(轻量级) | 进程(重量级) |
| 地址空间 | 共享父进程地址空间 | 复制父进程地址空间 |
| 创建速度 | 快(微秒级) | 慢(毫秒级) |
| 资源开销 | 小(仅栈和线程控制块) | 大(需复制页表等) |
| 相同点 | 都创建新的执行流,都并发执行 | --- |
| 不同点 | 资源共享,通讯简单 | 资源隔离,通讯复杂 |
| 注意点 | 需加锁保护共享数据 | 注意孤儿进程和僵尸进程 |
| 适用场景 | 高并发、需要共享数据的任务 | 需要独立运行环境的任务 |
四、线程的退出、等待与分离
一句话总结:线程干完活有三种结局------自己辞职(pthread_exit)、被老板等着收工(join)、或者散养不管(detach)。
4.1 生活类比:员工离职
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 函数 │ 生活类比 │ 关键点 │
├──────────────┼────────────────────────────────┼─────────┤
│ pthread_exit │ 员工主动提离职,交还工牌 │ 仅自己走 │
│ pthread_join │ 老板等员工交接完,打扫工位 │ 阻塞等待 │
│ pthread_detach│ 员工干完自动走,HR 自动清理 │ 不阻塞 │
│ exit() │ 整个公司倒闭,所有员工都得走 │ 全结束 │
└──────────────┴────────────────────────────────┴─────────┘
4.2 线程生命周期图
┌──────────┐
│ 创建 │ pthread_create
└────┬─────┘
│
▼
┌──────────┐
┌─────────│ 就绪 │←────────┐
│ └────┬─────┘ │
│ │ CPU 调度 │
│ ▼ │
│ ┌──────────┐ │
│ │ 运行 │──────────┤ 时间片用完
│ └────┬─────┘ │
│ │ │
│ ┌─────────┼─────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │阻塞 │ │退出 │ │分离 │ │
│ │等锁/IO│ │exit │ │detach │ │
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │
└────┘ ▼ ▼ │
┌──────────┐ ┌────────┐│
│等 join │ │系统自动 ││
│回收资源 │ │回收资源 ││
└──────────┘ └────────┘│
┌──────────────────────┘
│
▼
[*] 结束
4.3 pthread_exit(主动辞职)
c
#include <pthread.h>
void pthread_exit(void *retval); // 返回类型为 void,永不返回!
重要区别:
pthread_exit()→ 线程退出,进程继续exit()→ 进程退出,所有线程结束
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *worker(void *arg)
{
printf("【工人】干完活了,我先下班\n");
pthread_exit(NULL); // 仅自己退出,公司继续运营
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_join(tid, NULL);
printf("【老板】工人走了,公司继续运营\n");
return 0;
}
执行结果:
【工人】干完活了,我先下班
【老板】工人走了,公司继续运营
4.4 pthread_join(等待验收)
c
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
三大作用:① 等待线程退出 ② 获取返回值 ③ 释放资源(不 join 会内存泄漏)
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *worker(void *arg)
{
printf("【工人】开始搬砖...\n");
sleep(1);
printf("【工人】搬完了!\n");
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
printf("【老板】等着工人搬完...\n");
pthread_join(tid, NULL); // ← 阻塞!直到子线程结束
printf("【老板】工人搬完了,工位已清理\n");
return 0;
}
执行结果:
【老板】等着工人搬完...
【工人】开始搬砖...
【工人】搬完了!
【老板】工人搬完了,工位已清理
4.5 pthread_detach(散养模式)
c
#include <pthread.h>
int pthread_detach(pthread_t thread);
作用:设置线程为"散养"状态------退出时系统自动释放资源,无需 join。
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *logger(void *arg)
{
printf("【日志线程】开始记录日志...\n");
sleep(2);
printf("【日志线程】自动退出,资源自动释放\n");
return NULL;
}
int main(void)
{
pthread_t tid;
pthread_create(&tid, NULL, logger, NULL);
pthread_detach(tid); // ← 散养模式:退出自动回收
printf("【主程序】继续做其他事...\n");
sleep(3);
printf("【主程序】结束\n");
return 0;
}
执行结果:
【主程序】继续做其他事...
【日志线程】开始记录日志...
【日志线程】自动退出,资源自动释放
【主程序】结束
4.6 对比表
| 函数 | 功能 | 是否阻塞 | 获取返回值 | 资源释放 | 适用场景 |
|---|---|---|---|---|---|
pthread_create |
创建线程 | 不阻塞 | --- | --- | 分配任务 |
pthread_join |
等待退出 | 阻塞 | 可以 | 释放 | 需要结果 |
pthread_detach |
设置分离 | 不阻塞 | 不能 | 自动释放 | 只需执行 |
pthread_exit |
主动退出 | 不阻塞 | 传递返回值 | 需 join 或 detach | 主动结束 |
五、创建多个线程
一句话总结:创建多个线程就像给厨房增加多名厨师------可以让他们做不同的菜(不同函数),也可以做同样的菜(同一函数+不同参数)。
5.1 生活类比:厨房多厨师
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 模式 │ 生活类比 │ 代码实现 │
├──────────────┼────────────────────────────────┼─────────┤
│ 不同任务 │ 厨师做菜、服务员上菜、收银员收钱 │ 不同函数 │
│ 相同任务 │ 三个打包员都做同样的打包工作 │ 同一函数 │
│ 参数区分 │ 打包员1号、2号、3号编号不同 │ 参数不同 │
└──────────────┴────────────────────────────────┴─────────┘
5.2 多线程执行图
主线程(老板)
│
├── pthread_create ──→ 厨师1(做鱼香肉丝)──────┐
├── pthread_create ──→ 厨师2(做麻婆豆腐)──────┤
├── pthread_create ──→ 厨师3(做宫保鸡丁)──────┤
│ │
│ 三个厨师同时开工(并发执行) │
│ │
├── pthread_join(厨师1) ←───────────────────────┘
├── pthread_join(厨师2) ←───────────────────────┘
├── pthread_join(厨师3) ←───────────────────────┘
│
所有菜都做好了,打烊!
5.3 示例1:不同任务(不同岗位)
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *chef(void *arg)
{
printf("【厨师】我在炒菜\n");
return NULL;
}
void *waiter(void *arg)
{
printf("【服务员】我在上菜\n");
return NULL;
}
void *cashier(void *arg)
{
printf("【收银员】我在收钱\n");
return NULL;
}
int main(void)
{
pthread_t t1, t2, t3;
pthread_create(&t1, NULL, chef, NULL);
pthread_create(&t2, NULL, waiter, NULL);
pthread_create(&t3, NULL, cashier, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
printf("【店长】打烊下班!\n");
return 0;
}
可能结果(顺序每次不同):
【厨师】我在炒菜
【收银员】我在收钱
【服务员】我在上菜
【店长】打烊下班!
5.4 示例2:相同任务(多个下载任务)
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
typedef struct {
int id;
char name[64];
int size; // 下载大小(秒数)
} Task;
void *download(void *arg)
{
Task *t = (Task *)arg;
for (int i = 0; i < t->size; i++) {
printf("【任务%d】%s: %d/%d\n", t->id, t->name, i+1, t->size);
sleep(1);
}
printf("【任务%d】%s 下载完成!\n", t->id, t->name);
return NULL;
}
int main(void)
{
pthread_t tid[3];
Task tasks[3] = {
{1, "电影A.mp4", 3},
{2, "电影B.mp4", 2},
{3, "电影C.mp4", 4}
};
for (int i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, download, &tasks[i]);
for (int i = 0; i < 3; i++)
pthread_join(tid[i], NULL);
printf("\n全部下载完成!\n");
return 0;
}
可能结果(交替执行):
【任务1】电影A.mp4: 1/3
【任务2】电影B.mp4: 1/2
【任务3】电影C.mp4: 1/4
【任务2】电影B.mp4: 2/2
【任务2】电影B.mp4 下载完成!
【任务1】电影A.mp4: 2/3
【任务3】电影C.mp4: 2/4
...
5.5 注意事项对比表
| 注意事项 | 说明 | 错误后果 | 类比 |
|---|---|---|---|
| 统一由主线程创建 | 不要递归创建线程 | 管理混乱 | 老板统一招聘 |
| 相同任务用同一函数 | 通过参数区分 | 代码冗余 | 都是打包员,编号不同 |
| 参数生命周期管理 | 指针必须在线程使用期间有效 | 野指针、段错误 | 工单不能提前销毁 |
| 线程数组统一管理 | 用数组存 tid,统一 join | 遗漏 join 导致泄漏 | 花名册统一管理 |
六、线程间通讯
一句话总结:线程间通讯就是"传纸条"------主线程通过函数参数传给子线程(布置任务),子线程通过返回值传回主线程(汇报结果)。
6.1 生活类比:老板和员工
方向一:老板 → 员工(布置任务)
┌──────────┐ 参数 arg ┌──────────┐
│ 老板 │─────────────────────────>│ 员工 │
│ (主线程) │ "打印100份文件" │ (子线程) │
└──────────┘ └──────────┘
方向二:员工 → 老板(汇报结果)
┌──────────┐ pthread_exit ┌──────────┐
│ 老板 │<─────────────────────────│ 员工 │
│ (主线程) │ pthread_join 接收 │ (子线程) │
│ "收到" │ "打印完成100份" │ 返回值 │
└──────────┘ └──────────┘
6.2 示例:老板→员工(传参)
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *worker(void *arg)
{
int num = *(int *)arg; // 取出参数
printf("【员工】老板让我处理数字: %d\n", num);
printf("【员工】开始干活...\n");
return NULL;
}
int main(void)
{
pthread_t tid;
int num = 100; // 要传给子线程的数据
pthread_create(&tid, NULL, worker, &num);
pthread_join(tid, NULL); // ← 必须等子线程用完参数再退出!
return 0;
}
执行结果:
【员工】老板让我处理数字: 100
【员工】开始干活...
6.3 示例:员工→老板(返回结果)
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *calculator(void *arg)
{
static int result = 999; // ← 必须用 static!
printf("【员工】计算完成,汇报结果...\n");
pthread_exit((void *)&result);
}
int main(void)
{
pthread_t tid;
void *pret = NULL;
pthread_create(&tid, NULL, calculator, NULL);
pthread_join(tid, &pret); // 获取返回值
printf("【老板】员工汇报结果 = %d\n", *(int *)pret);
return 0;
}
执行结果:
【员工】计算完成,汇报结果...
【老板】员工汇报结果 = 999
6.4 经典错误:返回局部变量
错误写法:
┌──────────┐ ┌──────────┐
│ 子线程 │ int result = 100 │ 栈区域 │
│ │ pthread_exit(&r)──>│ result │
│ 退出后 │ │ 被回收 │
│ │ ← 主线程拿到的指针指向已回收内存!│
└──────────┘ └──────────┘
正确写法:
┌──────────┐ ┌──────────┐
│ 子线程 │ static int r=100 │ 数据段 │
│ │ pthread_exit(&r)──>│ result │
│ 退出后 │ │ 还在! │
│ │ ← 主线程安全访问! │ │
└──────────┘ └──────────┘
6.5 通讯对比表
| 方向 | 方式 | 代码要点 | 注意事项 | 适用场景 |
|---|---|---|---|---|
| 主→子 | pthread_create 第4参数 |
子线程用 (type*)arg 接收 |
必须用 pthread_join 确保子线程用完参数前主线程不退出 |
布置任务参数 |
| 子→主 | pthread_exit + pthread_join |
返回值必须 static 或全局变量 |
不能用局部变量(线程退出后栈被回收) | 汇报计算结果 |
| 双向 | 全局变量 + 互斥锁 | 读写时加锁保护 | 注意竞态条件 | 频繁交互场景 |
七、线程互斥锁
一句话总结:互斥锁就像厕所门上的"有人/无人"锁------一个线程进去锁门,其他人排队,防止多人同时用同一个资源导致数据混乱。
7.1 生活类比:ATM 取款
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 概念 │ 生活类比 │ 说明 │
├──────────────┼────────────────────────────────┼─────────┤
│ 共享资源 │ 一台 ATM 机 │ 只能一人用 │
│ 线程1 │ 你(取1000元) │ 先到先得 │
│ 线程2 │ 你老婆(取1000元) │ 排队等候 │
│ 不加锁 │ 两人同时操作 ATM │ 数据错乱!│
│ 加锁 │ ATM 门锁上,一人进一人出 │ 数据安全 │
│ 竞态条件 │ 取了1600,账户只扣了800 │ 银行亏了!│
└──────────────┴────────────────────────────────┴─────────┘
7.2 经典场景:银行取钱
不加锁的后果:
账户余额:1000元
│
┌──────────────┼──────────────┐
│ 你(取800) │ │ 老婆(取800)
▼ │ ▼
查余额 = 1000 │ 查余额 = 1000
│ │ │
计算: 1000-800=200 │ 计算: 1000-800=200
│ │ │
写回余额 = 200 ───────┼────── 写回余额 = 200
│ │ │
└──────────────┼──────────────┘
▼
最终余额 = 200 ← 错!
实际应扣 = 1600
银行亏了 800 元!
7.3 代码演示:不加锁的结果
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static int balance = 0; // 共享账户余额
void *deposit(void *arg)
{
int loops = *(int *)arg;
for (int i = 0; i < loops; i++) {
int tmp = balance; // 1. 读
tmp++; // 2. 改
balance = tmp; // 3. 写 ← 这三步不是原子的!
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops = atoi(argv[1]);
pthread_create(&t1, NULL, deposit, &loops);
pthread_create(&t2, NULL, deposit, &loops);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("余额 = %d(期望 = %d)\n", balance, 2 * loops);
return 0;
}
执行结果:
bash
$ ./a.out 100
余额 = 200(期望 = 200) ✅ 小规模,运气好
$ ./a.out 10000000
余额 = 10833956(期望 = 20000000) ❌ 少了900万!
7.4 竞态条件分析图
时间轴 → 线程1(你) 线程2(老婆) 内存(balance)
│ │ │ balance = 0
│ │ │
├─── │ tmp = balance(0) ──────►│ balance = 0
│ │ tmp++ => tmp = 1 │
├─── │ │ tmp = balance(0)►balance = 0
│ │ │ tmp++ => tmp=1
├─── │ balance = 1 ◄───────────│ balance = 1
│ │ │
├─── │ │ balance = 1 ◄─── balance = 1
│ │ │
▼ ▼ ▼
结果:明明加了两次,balance = 1 才对!
但实际 balance = 1,因为"读-改-写"交错了!
7.5 解决方案:加互斥锁
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static int balance = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 初始化锁
void *deposit(void *arg)
{
int loops = *(int *)arg;
for (int i = 0; i < loops; i++) {
pthread_mutex_lock(&lock); // 加锁
int tmp = balance;
tmp++;
balance = tmp;
pthread_mutex_unlock(&lock); // 解锁
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops = atoi(argv[1]);
pthread_create(&t1, NULL, deposit, &loops);
pthread_create(&t2, NULL, deposit, &loops);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("余额 = %d(期望 = %d)\n", balance, 2 * loops);
return 0;
}
执行结果:
bash
$ ./a.out 10000000
余额 = 20000000(期望 = 20000000) ✅ 始终正确!
7.6 互斥锁工作原理
锁变量 v 初始值 = 1(表示"可用")
线程A想进入临界区 ──→ 检查 v ? ──→ v=1,可用!
│
线程A获得锁
│
设置 v = 0(锁住)
│
线程A访问临界资源
│
线程A释放锁
│
设置 v = 1(解锁)
│
唤醒等待的线程B
│
线程B获得锁 ...
┌─────────────────────┐
│ 锁状态 │
├──────────┬──────────┤
│ 未锁定 │ v = 1 │
│ 已锁定 │ v = 0 │
└──────────┴──────────┘
7.7 死锁
生活类比:两个人过窄巷子------你侧身让左边,他也侧身让左边,两人堵住了;你又让右边,他也让右边,还是堵住。两人互相等对方先让,结果谁也过不去。
死锁场景:
线程1 线程2
│ │
│ 持有锁A │ 持有锁B
│ 等待锁B │ 等待锁A
│ │
└──────────┬─────────────┘
▼
两人互相等,死锁了!
避免死锁:所有线程按相同顺序加锁(先A后B)
7.8 对比表
| 对比维度 | 无锁 | 有锁(互斥锁) |
|---|---|---|
| 数据正确性 | 可能错误(竞态条件) | 始终正确 |
| 执行性能 | 高(无锁开销) | 有锁竞争开销 |
| 相同点 | 都能实现多线程并发访问共享数据 | --- |
| 不同点 | 不加保护,数据可能错乱 | 加锁保护,数据安全 |
| 注意点 | 小规模循环可能"运气好"不出错 | 注意死锁和锁粒度 |
| 适用场景 | 只读操作、各线程独立数据 | 多个线程写同一共享数据 |
八、生产者-消费者模型
一句话总结:生产者-消费者就是"奶茶店------店员做好了喊一声,顾客听到再来拿",而不是顾客不停地跑来问"好了没?"
8.1 生活类比:奶茶店
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 概念 │ 生活类比 │ 说明 │
├──────────────┼────────────────────────────────┼─────────┤
│ 生产者线程 │ 做奶茶的店员 │ 生产产品 │
│ 消费者线程 │ 喝奶茶的顾客 │ 消费产品 │
│ 共享仓库 │ 制作台上的成品区 │ 存放产品 │
│ number │ 成品区里的奶茶杯数 │ 产品数量 │
│ 忙等 │ 顾客每隔5秒问一次"好了没" │ 浪费CPU │
│ 条件变量 │ 店员喊"XX号奶茶好了!" │ 高效通知 │
└──────────────┴────────────────────────────────┴─────────┘
8.2 模型架构图
┌───────┐
│ 生产者1 │──┐
└───────┘ │ ┌──────────────┐ ┌───────┐
┌───────┐ ├───►│ │◄───┤ 消费者1│
│ 生产者2 │──┤ │ 共享仓库 │ └───────┘
└───────┘ │ │ (number) │ ┌───────┐
┌───────┐ ├───►│ │◄───┤ 消费者2│
│ 生产者3 │──┘ └──────────────┘ └───────┘
└───────┘
8.3 基于互斥锁的实现(有缺陷)
c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
#include <unistd.h>
static int number = 0; // 仓库产品数
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *producer(void *arg)
{
int cnt = atoi((char *)arg);
for (int i = 0; i < cnt; i++) {
pthread_mutex_lock(&lock);
printf("【店员】做了一杯奶茶,库存: %d\n", ++number);
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
int total = 0, consumed = 0;
bool done = false;
for (int i = 1; i < argc; i++) {
total += atoi(argv[i]);
pthread_create(&tid, NULL, producer, (void *)argv[i]);
}
// 消费者(主线程)
for (;;) {
pthread_mutex_lock(&lock);
while (number > 0) {
consumed++;
printf("【顾客】喝了一杯奶茶,库存: %d\n", --number);
done = consumed >= total;
sleep(1); // ← 忙等!浪费CPU
}
pthread_mutex_unlock(&lock);
if (done) break;
}
return 0;
}
三大缺陷:
| 缺陷 | 后果 |
|---|---|
| CPU 忙等 | 顾客不断问"好了没",CPU空转 |
| 轮询延迟 | sleep(1) 导致消费不及时 |
| 资源泄漏 | 生产者没被 join |
九、条件变量
一句话总结:条件变量就是"叫醒服务"------线程可以安心睡觉,等其他线程有了"好消息"再叫醒它,不用自己反复醒来查看。
9.1 生活类比:快递柜取件
┌─────────────────────────────────────────────────────────┐
│ 生 活 类 比 对 照 表 │
├──────────────┬────────────────────────────────┬─────────┤
│ 概念 │ 生活类比 │ 说明 │
├──────────────┼────────────────────────────────┼─────────┤
│ 条件变量 │ 快递柜的通知系统 │ 叫醒服务 │
│ pthread_cond │ 快递柜的广播喇叭 │ 通知机制 │
│ .cond_wait │ 用户在家睡觉,等通知 │ 阻塞等待 │
│ .cond_signal │ 快递员按喇叭:有快递了! │ 唤醒一个 │
│ .cond_broadca │ 快递员喊:所有人都有快递! │ 唤醒所有 │
│ 忙等 │ 用户每隔5分钟去开柜子看 │ 浪费精力 │
│ 条件变量 │ 用户睡觉,快递员打电话通知 │ 高效! │
└──────────────┴────────────────────────────────┴─────────┘
9.2 忙等 vs 条件变量
忙等(Busy Waiting):
顾客:奶茶好了吗? → 没有
顾客:奶茶好了吗? → 没有
顾客:奶茶好了吗? → 没有
顾客:奶茶好了吗? → 好了! ← 终于等到,但CPU已经浪费了!
条件变量(Condition Variable):
顾客:没有奶茶,我先睡会儿(阻塞等待)
店员:做好了一杯!叫醒顾客(pthread_cond_signal)
顾客:被叫醒,喝奶茶!
9.3 条件变量 API
| 函数 | 功能 |
|---|---|
pthread_cond_init |
初始化条件变量 |
pthread_cond_destroy |
销毁条件变量 |
pthread_cond_wait |
阻塞等待(自动释放锁,唤醒后自动获取锁) |
pthread_cond_signal |
唤醒 一个 等待线程 |
pthread_cond_broadcast |
唤醒 所有 等待线程 |
9.4 条件变量工作机制
消费者(顾客) 生产者(店员)
│ │
│ 获取锁 lock │
│ │
│ 检查 number == 0? │
│ 是,没产品 │
│ │
│ pthread_cond_wait(&cond, &lock) │
│ ├─ 自动释放锁 │
│ ├─ 进入睡眠 │
│ │ │
│ │ 获取锁 lock │
│ │ 生产产品 │
│ │ 释放锁 │
│ │ signal(&cond) ──┤
│ │ │
│ ├─ 被唤醒 ◄──────────────────────────┘
│ ├─ 自动重新获取锁 │
│ │
│ 再次检查 number > 0? │
│ 是,消费产品 │
│ │
│ 释放锁 lock │
9.5 完整代码:基于条件变量
c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <pthread.h>
static int number = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *producer(void *arg)
{
int cnt = atoi((char *)arg);
for (int i = 0; i < cnt; i++) {
pthread_mutex_lock(&lock);
printf("【店员】做了一杯奶茶,库存: %d\n", ++number);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond); // 叫醒顾客
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t tid;
int total = 0, consumed = 0;
bool done = false;
for (int i = 1; i < argc; i++) {
total += atoi(argv[i]);
pthread_create(&tid, NULL, producer, (void *)argv[i]);
}
for (;;) {
pthread_mutex_lock(&lock);
while (number == 0) // 用 while 不是 if!
pthread_cond_wait(&cond, &lock); // 没奶茶,睡觉
while (number > 0) {
consumed++;
printf("【顾客】喝了一杯奶茶,库存: %d\n", --number);
done = consumed >= total;
}
pthread_mutex_unlock(&lock);
if (done) break;
}
return 0;
}
执行结果:
bash
$ ./a.out 3 4
【店员】做了一杯奶茶,库存: 1
【顾客】喝了一杯奶茶,库存: 0
【店员】做了一杯奶茶,库存: 1
【顾客】喝了一杯奶茶,库存: 0
...
9.6 两个关键问题
Q1:为什么用 while 不用 if?
if (number == 0)
pthread_cond_wait(&cond, &lock);
// 如果这里被"虚假唤醒"(没有信号也能醒),
// number 还是 0,但程序继续执行,会消费不存在的产品!
while (number == 0)
pthread_cond_wait(&cond, &lock);
// 唤醒后再检查一次,确保真的有产品!
Q2:信号丢失问题
消费者 生产者
│ │
│ 检查条件 → 不满足 │
│ │ 发送信号 → 条件满足了!
│ │
│ 开始等待 → 永远等不到! │
│ ← 信号丢失了! │
│ │
pthread_cond_wait 的原子操作解决了这个问题:
"释放锁 + 进入等待"是不可分割的,信号不会丢失。
9.7 对比表
| 对比维度 | 互斥锁(Mutex) | 条件变量(Cond) |
|---|---|---|
| 核心作用 | 保护共享资源的互斥访问 | 实现线程间的等待/通知机制 |
| 是否"持有"资源 | 是(锁住临界区) | 否(只是信号机制) |
| 阻塞方式 | 主动竞争锁 | 主动睡眠等待 |
| 相同点 | 都是线程同步机制 | --- |
| 不同点 | 解决"互斥"问题 | 解决"等待/通知"问题 |
| 注意点 | 注意死锁和锁粒度 | 必须配合互斥锁,注意虚假唤醒 |
| 适用场景 | 任何写共享数据的场景 | 生产者-消费者模型 |
十、综合知识对比表
10.1 进程 vs 线程
| 维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 独立(隔离) | 共享(同一进程内) |
| 创建开销 | 大 | 小 |
| 切换速度 | 慢 | 快 |
| 通讯方式 | 复杂(IPC) | 简单(全局变量) |
| 相同点 | 都有独立执行流、都有ID | --- |
| 注意点 | 进程间通讯要防死锁 | 多线程要防竞态条件 |
| 适用场景 | 高隔离、高可靠性 | 高并发、低延迟、数据共享 |
10.2 创建 vs 等待 vs 分离 vs 退出
| 函数 | 功能 | 阻塞? | 获取返回值? | 资源释放 | 注意点 |
|---|---|---|---|---|---|
pthread_create |
创建线程 | 否 | --- | --- | 需 -lpthread |
pthread_join |
等待退出 | 是 | 可以 | 释放 | 必须调用 |
pthread_detach |
设置分离 | 否 | 不能 | 自动释放 | 分离后不能 join |
pthread_exit |
主动退出 | 否 | 传递返回值 | 需 join/detach | 主线程调了不结束进程 |
10.3 互斥锁 vs 条件变量
| 维度 | 互斥锁 | 条件变量 |
|---|---|---|
| 作用 | 保护互斥访问 | 实现等待/通知 |
| 阻塞方式 | 竞争锁 | 等待条件 |
| 相同点 | 都是线程同步机制 | --- |
| 不同点 | 解决"互斥" | 解决"等待/通知" |
| 注意点 | 防死锁 | 必须配合互斥锁,防虚假唤醒 |
| 适用场景 | 写共享数据 | 生产者-消费者 |
10.4 signal vs broadcast
| 维度 | pthread_cond_signal |
pthread_cond_broadcast |
|---|---|---|
| 唤醒数量 | 一个 | 所有 |
| 适用场景 | 等待线程执行相同任务 | 等待线程执行不同任务 |
| 性能 | 较高 | 较低 |
| 相同点 | 都用于唤醒等待线程 | --- |
| 注意点 | 唤醒的线程由调度决定 | 所有线程重新竞争锁 |
| 典型用例 | 单消费者 | 多消费者 |
10.5 忙等 vs 条件变量
| 维度 | 忙等 | 条件变量 |
|---|---|---|
| CPU 使用率 | 极高(空转) | 近乎 0(睡眠) |
| 响应及时性 | 取决于轮询频率 | 立即响应 |
| 代码复杂度 | 简单 | 稍复杂 |
| 相同点 | 都能等待条件满足 | --- |
| 注意点 | 轮询间隔不好控制 | 必须配互斥锁,防虚假唤醒 |
| 推荐 | 不推荐 | 强烈推荐 |
10.6 无锁 vs 有锁
| 维度 | 无锁 | 有锁 |
|---|---|---|
| 数据正确性 | 可能错误 | 始终正确 |
| 执行性能 | 高 | 有锁竞争开销 |
| 相同点 | 都能并发访问共享数据 | --- |
| 不同点 | 不加保护 | 加锁保护 |
| 注意点 | 小规模可能"运气好" | 防死锁 |
| 适用场景 | 只读、独立数据 | 多线程写同一数据 |
10.7 Joinable vs Detached
| 维度 | 可结合(Joinable) | 可分离(Detached) |
|---|---|---|
| 资源回收 | 需 join | 系统自动 |
| 获取返回值 | 可以 | 不能 |
| 默认状态 | 是 | 需显式调用 |
| 泄漏风险 | 不 join 会泄漏 | 无风险 |
| 相同点 | 都是正常生命周期管理 | --- |
| 注意点 | 必须 join | 分离后不能 join |
| 适用场景 | 需要结果 | 只需执行 |
十一、常见面试题与易错点
Q1:pthread_exit 和 exit() 有什么区别?
pthread_exit() |
exit() |
|
|---|---|---|
| 作用范围 | 仅当前线程 | 整个进程 |
| 进程状态 | 继续运行 | 终止 |
| 类比 | 员工辞职,公司继续 | 公司倒闭 |
Q2:子线程传返回值为什么不能用局部变量?
c
// 错误:局部变量在线程栈上,线程退出后栈被回收
void *bad(void *arg) {
int result = 100;
pthread_exit(&result); // 野指针!
}
// 正确:static 变量在数据段,不会随线程退出回收
void *good(void *arg) {
static int result = 100;
pthread_exit(&result);
}
Q3:什么是虚假唤醒?
即使没有信号,pthread_cond_wait 也可能返回。必须用 while 循环检查条件:
c
while (number == 0) // 正确
pthread_cond_wait(...);
if (number == 0) // 错误
pthread_cond_wait(...);
Q4:死锁四条件
| 条件 | 类比 |
|---|---|
| 互斥 | 一把钥匙只能给一个人 |
| 持有并等待 | 拿着钥匙A等钥匙B |
| 不可剥夺 | 钥匙不能从手里抢走 |
| 循环等待 | A等B的钥匙,B等A的钥匙 |
Q5:什么是竞态条件?
多个线程同时访问共享数据,结果取决于 CPU 调度顺序。就像两个人同时取钱,都查到余额 1000,都取 800,结果只扣了 800。
Q6:pthread_create 失败原因
| 错误码 | 含义 | 解决 |
|---|---|---|
EAGAIN |
资源不足/线程数上限 | 减少线程数 |
EINVAL |
参数无效 | 检查 attr 设置 |
EPERM |
权限不足 | 检查调度策略 |
附录:核心函数速查表
| 函数 | 头文件 | 作用 |
|---|---|---|
pthread_create |
<pthread.h> |
创建线程 |
pthread_exit |
<pthread.h> |
线程退出(void,永不返回) |
pthread_join |
<pthread.h> |
等待线程退出并释放资源 |
pthread_detach |
<pthread.h> |
设置线程为分离状态 |
pthread_self |
<pthread.h> |
获取当前线程 ID |
pthread_mutex_init |
<pthread.h> |
初始化互斥锁 |
pthread_mutex_destroy |
<pthread.h> |
销毁互斥锁 |
pthread_mutex_lock |
<pthread.h> |
加锁(阻塞等待) |
pthread_mutex_unlock |
<pthread.h> |
解锁 |
pthread_cond_init |
<pthread.h> |
初始化条件变量 |
pthread_cond_destroy |
<pthread.h> |
销毁条件变量 |
pthread_cond_wait |
<pthread.h> |
等待条件变量(释放锁+睡眠) |
pthread_cond_signal |
<pthread.h> |
唤醒一个等待线程 |
pthread_cond_broadcast |
<pthread.h> |
唤醒所有等待线程 |
编译命令 :
gcc program.c -lpthread -o program