目录
-
- 一、为什么需要同步?
-
- [1. 临界资源与临界区](#1. 临界资源与临界区)
- [2. 互斥与原子性](#2. 互斥与原子性)
- [3. 竞态条件示例 ------ 售票系统](#3. 竞态条件示例 —— 售票系统)
- 二、互斥量(Mutex)
-
- [1. 初始化与销毁](#1. 初始化与销毁)
- [2. 加锁与解锁](#2. 加锁与解锁)
- [3. 改进售票系统](#3. 改进售票系统)
- [4. 互斥量的底层原理](#4. 互斥量的底层原理)
- 三、可重入与线程安全
-
- [1. 概念区分](#1. 概念区分)
- [2. 常见不安全与不可重入情况](#2. 常见不安全与不可重入情况)
- [3. 联系与区别](#3. 联系与区别)
- 四、死锁
-
- [1. 定义](#1. 定义)
- [2. 四个必要条件](#2. 四个必要条件)
- [3. 避免死锁的方法](#3. 避免死锁的方法)
- [五、条件变量(Condition Variable)](#五、条件变量(Condition Variable))
-
- [1. 为什么需要条件变量?](#1. 为什么需要条件变量?)
- [2. 条件变量函数](#2. 条件变量函数)
- [3. 为什么 `pthread_cond_wait` 需要互斥量?](#3. 为什么
pthread_cond_wait需要互斥量?) - [4. 条件变量使用规范](#4. 条件变量使用规范)
- [5. 简单示例](#5. 简单示例)
- 六、总结
一、为什么需要同步?
1. 临界资源与临界区
- 临界资源:多线程共享的资源(如全局变量、文件、设备等)。
- 临界区:访问临界资源的代码段,需要互斥执行。
2. 互斥与原子性
- 互斥:保证同一时刻只有一个线程进入临界区。
- 原子操作:不可被中断的操作,要么全部完成,要么全部未完成。
3. 竞态条件示例 ------ 售票系统
下面的代码模拟了四个线程同时售卖 100 张票,未加任何保护:
c
int ticket = 100;
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
if (ticket > 0) {
usleep(1000); // 模拟业务处理
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
运行结果可能输出负数票(如 -1、-2),这明显是错误的。原因在于:
ticket > 0判断与ticket--操作不是原子的。- 汇编层面,
ticket--对应三条指令:load(加载到寄存器)、update(减1)、store(写回内存)。 - 线程可能在执行完
load后被切换,导致其他线程读到旧值,最终超卖。
结论:必须引入同步机制,保证对临界资源的互斥访问。
二、互斥量(Mutex)
互斥量是 Linux 中最基本的互斥锁,用于保护临界区。
1. 初始化与销毁
静态初始化(适用于全局或静态变量):
c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:
c
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// attr 传 NULL 使用默认属性
销毁:
c
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:使用静态初始化的互斥量不需要销毁;不要销毁已加锁的互斥量。
2. 加锁与解锁
c
int pthread_mutex_lock(pthread_mutex_t *mutex); // 阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 非阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex);
lock:若锁已被占用,则阻塞等待。unlock:释放锁,唤醒等待的线程。

3. 改进售票系统
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
pthread_mutex_lock(&mutex);
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return NULL;
}
int main() {
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
现在,票数永远不会出现负数,因为每次只有一个线程进入临界区。
4. 互斥量的底层原理

互斥量的实现依赖 CPU 提供的原子交换指令(如 x86 的 xchg 或 cmpxchg)。一条指令完成寄存器和内存单元的交换,保证"测试并设置"的原子性。多核平台上,总线锁机制确保同一时刻只有一个处理器能执行该指令。
三、可重入与线程安全
1. 概念区分
- 线程安全:多个线程并发执行同一函数,结果可预期,不会出现数据错误。
- 可重入:函数在被中断后再次被调用(重入),结果仍然正确。
2. 常见不安全与不可重入情况
| 情况 | 说明 |
|---|---|
| 使用全局或静态变量(未加锁) | 线程不安全、不可重入 |
调用 malloc / free |
使用全局堆管理结构,不可重入 |
调用标准 I/O(printf、fopen) |
内部使用全局缓冲区 |
| 返回指向静态数据的指针 | 多线程调用会覆盖数据 |
3. 联系与区别
- 可重入函数一定是线程安全的。
- 线程安全函数不一定是可重入的(例如加锁保护的函数,若重入时锁未释放,会死锁)。
- 纯局部变量(不访问全局数据)的函数是可重入且线程安全的。
示例:
c
// 线程安全但不可重入(因为使用了锁)
int safe_inc() {
static int counter = 0;
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
return counter;
}
如果在持有锁期间再次调用该函数(同一线程重入),就会死锁。因此加锁函数不是可重入的。
四、死锁
1. 定义
死锁:两个或多个线程互相等待对方持有的资源,导致永远阻塞。
2. 四个必要条件
- 互斥:资源一次只能被一个线程占用。
- 请求与保持:线程持有资源的同时请求其他资源。
- 不剥夺:资源只能由持有者主动释放。
- 循环等待:线程间形成循环等待链。
3. 避免死锁的方法
- 破坏四个必要条件之一(例如,资源一次性分配)。
- 保持一致的加锁顺序(所有线程按相同顺序获取锁)。
- 使用
pthread_mutex_trylock避免阻塞。 - 使用银行家算法或死锁检测(较复杂,一般用于操作系统内部)。
五、条件变量(Condition Variable)
互斥锁解决了互斥问题,但无法解决"同步"问题 ------ 即让线程按特定顺序执行。条件变量允许线程等待某个条件成立,并在条件满足时被唤醒。
1. 为什么需要条件变量?
考虑生产者-消费者模型:消费者需要等待队列非空才能取数据。如果使用互斥锁 + 轮询,会浪费 CPU;使用条件变量,消费者可以在队列为空时阻塞,直到生产者插入数据后唤醒它。
2. 条件变量函数
c
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待线程
3. 为什么 pthread_cond_wait 需要互斥量?
条件变量必须与互斥量配合使用。

原因在于:
- 判断条件(如队列是否为空)需要访问共享数据,必须加锁。
- 如果先解锁再等待,可能会错过信号(在解锁后、等待前,条件可能已被满足并发出信号)。
pthread_cond_wait内部会原子地完成:解锁互斥量 + 阻塞等待 + 被唤醒后重新加锁。
错误用法示例:
c
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond, &mutex); // 这里 mutex 未锁定,行为未定义
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
上述代码中,解锁与等待不是原子操作,可能丢失信号。
4. 条件变量使用规范
等待线程:
c
pthread_mutex_lock(&mutex);
while (条件为假) {
pthread_cond_wait(&cond, &mutex);
}
// 条件满足,执行操作
pthread_mutex_unlock(&mutex);
唤醒线程:
c
pthread_mutex_lock(&mutex);
设置条件为真;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
注意:使用
while而非if检查条件,因为可能发生虚假唤醒(spurious wakeup)。
5. 简单示例
c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *waiter(void *arg) {
pthread_mutex_lock(&mutex);
printf("waiter: waiting...\n");
pthread_cond_wait(&cond, &mutex);
printf("waiter: awakened!\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
void *signaler(void *arg) {
sleep(2);
pthread_mutex_lock(&mutex);
printf("signaler: sending signal\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, waiter, NULL);
pthread_create(&t2, NULL, signaler, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
运行结果:
waiter: waiting...
signaler: sending signal
waiter: awakened!
六、总结
| 知识点 | 核心内容 |
|---|---|
| 同步与互斥 | 临界资源、临界区、原子性、竞态条件 |
| 互斥量 | pthread_mutex_lock/unlock,保护临界区 |
| 线程安全 | 多线程并发执行结果可预期 |
| 可重入 | 函数被重入后仍正确,可重入函数一定线程安全 |
| 死锁 | 四个必要条件,避免方法(加锁顺序一致等) |
| 条件变量 | 配合互斥量实现同步,pthread_cond_wait/signal,使用规范 |