Linux 多线程编程——总结

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、都可被调度 ---
不同点 资源隔离、切换慢、通讯复杂 资源共享、切换快、通讯简单
注意点 进程间通讯需注意死锁 多线程需注意竞态条件
适用场景 需要高隔离、高可靠性的任务 需要高并发、低延迟、数据共享的任务

二、线程相关命令

一句话总结 :用 pidstattopps 三个命令,像查户口一样查清进程内部有多少线程在干活。

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

相关推荐
tmlx3I0811 小时前
Tomcat的架构设计和启动过程详解
java·tomcat
名字还没想好☜1 小时前
Docker 多阶段构建:把镜像从 1.2GB 砍到 15MB
运维·docker·容器·多阶段构建·镜像优化
weixin_422329311 小时前
AgentScope Java 项目入门 & Builder 深度解读
java·开发语言·人工智能
祸心依旧1 小时前
Visual Studio 五月更新 —— 计划、评审、优化
运维·人工智能·visual studio
shepherd1261 小时前
一次把 Spring MVC 文件上传参数“查没了”的排查:multipart、Filter 与 request body 的连环坑
java·后端·spring·mvc
宠友信息2 小时前
Spring Boot与异步审核构建仿小红书源码内容发布全流程
java·数据库·spring boot·redis·mysql·oracle·uni-app
Web4Browser2 小时前
Headless 浏览器自动化适合哪些任务?巡检、截图和提交动作要分级
运维·前端·自动化
Listen·Rain2 小时前
Springboot整合RedisStack
java·spring boot·redis
難釋懷2 小时前
Nginx浏览器强制缓存
运维·nginx·缓存