一、线程共享资源基础
同一进程下的多个线程:
- 共享:文本段(代码段)、数据段、堆区、文件描述符
- 私有:每个线程独立栈区、寄存器、线程局部数据
共享内存带来便利的同时,会产生资源竞争(竞态条件):多个线程同时读写同一份共享数据,会出现数据错乱。
基础概念
- 原子操作:不能被 CPU 调度打断的最小操作,一旦开始就必须执行完毕。
注意:C 语言普通
i++不是原子操作,分为读‑改‑写三步,多线程下会出错。
- 临界区(临界代码):被加锁、解锁包裹的代码片段。同一时刻只允许一个线程执行临界区代码,用来保护共享资源。
二、互斥锁 pthread_mutex_t
核心作用
实现共享资源互斥访问,保证同一时刻仅有一个线程进入临界区。把非原子的读‑改‑写操作包装成逻辑上不可分割的整体,解决竞态条件,保证并发下数据一致性。
锁的代价
✅ 优点:把非线程安全的并发代码变为安全,防止脏读、数据丢失更新,可以控制执行顺序。
❌ 代价:带来性能开销;使用不当会造成死锁,程序并发能力下降。
互斥锁工作原理
互斥锁依靠底层原子操作实现排他性。一个线程成功加锁后,其他线程再次加锁会被操作系统阻塞,让出 CPU;直到持有锁的线程执行完临界区调用解锁,操作系统才唤醒等待的线程。
- 一个进程可以创建任意数量互斥锁
- 普通互斥锁不支持重复加锁 :同一个线程拿到锁之后,再次调用
lock会直接卡死;必须先解锁,才能再次加锁。
错误示例:锁加完,条件分支内才解锁,分支走不到就永久死锁
c
pthread_mutex_lock(&lock);
if (0) {
pthread_mutex_unlock(&lock); // 永远执行不到,锁不释放,程序卡死
}
API 接口
头文件:#include <pthread.h>
- 定义锁(一般全局或者堆上分配)
c
pthread_mutex_t lock;
- 初始化锁
c
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex:锁变量地址attr:锁属性,传NULL使用默认属性- 返回:0 成功,非 0 代表错误
- 上锁(阻塞版)
c
int pthread_mutex_lock(pthread_mutex_t *mutex);
拿不到锁就阻塞休眠,直到锁被释放。
- 解锁
c
int pthread_mutex_unlock(pthread_mutex_t *mutex);
注意:只有持有锁的线程才可以解锁,其他线程调用会报错
EPERM。
- 销毁锁
c
int pthread_mutex_destroy(pthread_mutex_t *mutex);
如果锁还被占用,销毁返回
EBUSY。
三种加锁方式对比
| 函数 | 特性 | 适用场景 |
|---|---|---|
pthread_mutex_lock() |
阻塞,拿不到锁就休眠 | 绝大多数常规业务 |
pthread_mutex_trylock() |
非阻塞,拿不到锁立刻返回,不阻塞 | 不想卡死,拿不到锁就做别的逻辑 |
pthread_mutex_timedlock() |
带超时的阻塞等待 | 防止死锁,设置最大等待时间 |
死锁
死锁:多个线程互相持有对方需要的锁,全部线程阻塞,程序不再推进。
死锁四个必要条件(全部满足才会死锁)
- 互斥条件:资源同一时刻只能一个线程使用
- 不可剥夺条件:已经占有的锁,别人不能强行抢走
- 请求与保持:线程已经持有锁,同时申请别的锁
- 循环等待:线程之间形成锁的循环依赖
避免死锁方案
- 使用
trylock尝试加锁,失败就释放已经拿到的锁 - 所有线程保持统一的加锁顺序,不要交叉申请锁
三、信号量 sem_t
信号量本质代表有限数量的资源 ,可以用来做互斥,也可以做线程间同步。
互斥锁只允许最多 1 个线程进入临界区;信号量初始值可以大于 1,允许多个线程同时访问资源。
头文件:#include <semaphore.h>
API
sem_init初始化信号量
c
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:信号量变量指针pshared:0用于线程之间;非 0 用于进程之间(搭配共享内存)value:初始可用资源数目- 返回:0 成功,‑1 失败,设置
errno
sem_destroy销毁信号量
c
int sem_destroy(sem_t *sem);
sem_wait():申请资源,信号量‑1
c
int sem_wait(sem_t *sem);
信号量 > 0:直接减 1 返回;信号量等于 0:线程阻塞等待,直到有资源释放。
sem_post():释放资源,信号量+1
c
int sem_post(sem_t *sem);
原子性把信号量 +1,同时唤醒一个正在等待该信号量的线程。
小提示:
- 信号量初始值设为 1,效果等价互斥锁,可以实现互斥访问;
- 初始值大于 1,实现"允许 N 个线程同时访问资源"。
互斥锁 vs 信号量简单区分
- 互斥锁(mutex):专门做互斥,谁加锁,谁必须负责解锁;所有权概念。
- 信号量(sem_t) :资源计数器,没有所有者概念,任意线程都可以
post释放资源,既可以互斥,也可以做线程同步、生产者消费者模型。
编译提示
使用线程库编译,必须链接 pthread 库
bash
gcc demo.c -o demo -pthread
笔记要点记忆:
- mutex:锁,有所有者,重复加锁直接死锁;
- sem_wait 减资源,sem_post 加资源,无所有者;
- 死锁四个条件,统一加锁顺序是最常用规避手段。