线程同步与互斥总结
本章主线是“先互斥,再同步”:
| 1. 线程互斥(本节) | 同一时刻只能有一个执行流进入临界区 | 互斥量 pthread_mutex_t |
| 2. 线程同步(后续) | 按特定顺序访问,避免饥饿 | 条件变量、信号量 |
课件本节四个目标:1-1 背景概念 → 1-2 互斥量 mutex → 1-3 实现原理 → 1-4 封装。
1. 五个背景概念
| 共享资源 | 多个线程都能访问的资源 |
| 临界资源 | 多线程执行流中被保护的共享资源 |
| 临界区 | 每个线程内部访问临界资源的那段代码 |
| 互斥 | 任何时刻保证有且只有一个执行流进入临界区、访问临界资源,通常对临界资源起保护作用 |
| 原子性 | 不会被任何调度机制打断的操作;只有两态:要么完成,要么未完成(如何实现留待 1-3 讨论) |
2. 为什么需要互斥:售票系统反例
ticket = 100 全局变量,4 个线程各自 while(1):
while (1) {
if (ticket > 0) {
usleep(1000); // 模拟漫长的业务过程
printf("%s sells ticket:%d\\n", id, ticket);
ticket—; // 非原子
} else break;
}
一次真实运行结果:thread 4 卖到 1,thread 2 卖 0,thread 1 卖 -1,thread 3 卖 -2 —— 既重复超卖又卖出负数票。
原因逐条拆解如下:
mov 0x2004e3(%rip),%eax # load :从内存读到寄存器
sub $0x1,%eax # update :寄存器内 -1
mov %eax,0x2004da(%rip) # store :写回内存
注意:
- 代码必须要有互斥行为:进入临界区执行时不允许其他线程进入;
- 多个线程同时要求执行临界区、而临界区无人执行时,只能允许一个线程进入;
- 线程不在临界区中执行时,不能阻止其他线程进入。
本质结论:需要一把锁,Linux 上它就是互斥量。
3. 互斥量接口
初始化
// 方法1 静态分配(全局/静态)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 方法2 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// mutex:要初始化的互斥量 attr:NULL
销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
三条注意:① 用 PTHREAD_MUTEX_INITIALIZER 初始化的不需要销毁;② 不要销毁一个已经加锁的互斥量;③ 已销毁的互斥量要确保后续没有线程再尝试加锁。
加锁 / 解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 返回值:成功返回 0,失败返回错误号
pthread_mutex_lock 的两种情形:
- 互斥量未锁 → 直接锁定并返回成功;
- 已被其他线程锁定,或同时有其他线程申请但没竞争到 → 调用陷入阻塞(执行流被挂起),等待互斥量解锁。
改造后的售票代码
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 之前也必须解锁
break;
}
}
return NULL;
}
int main(void) {
pthread_mutex_init(&mutex, NULL);
/* create t1..t4, join t1..t4 */
pthread_mutex_destroy(&mutex);
}
锁必须覆盖 if 判断 + ticket– 整个逻辑,只锁 ticket– 依然会超卖。
4. 互斥量实现原理
- i++ / ++i 都不是原子的,直接共享会带来数据一致性问题;
- 为实现互斥锁,大多数体系结构都提供 swap 或 exchange 指令,作用是把寄存器与内存单元的数据相交换;
- 因为只有一条指令,保证了原子性:即使多处理器平台,访问内存的总线周期也有先后,一个处理器执行交换指令时,另一个处理器的交换指令只能等总线周期。
lock: ; 加锁
mov $0, %al
xchg %al, mutex ; 原子交换
if (%al > 0) return 0 ; 原来 mutex==1,交换成功拿到锁
else { 挂起等待; goto lock; }
unlock: ; 解锁
mov $1, %al
xchg %al, mutex
if (%al > 0) { 唤醒等待该 mutex 的线程; return 0; }
return 0;
要点:所谓“加锁”就是一次原子交换;没换到(拿到 0)就说明锁被别人持着,只能挂起等待 —— 这就是 mutex 的底层机制。
5. 互斥量的 C++ 封装(1-4)
Lock.hpp,命名空间 LockModule,两个类:
① Mutex(可独立使用的锁本体)
class Mutex {
public:
Mutex(const Mutex &) = delete; // 禁止拷贝
const Mutex &operator=(const Mutex &) = delete; // 禁止赋值
Mutex() { pthread_mutex_init(&_mutex, nullptr); }
void Lock() { pthread_mutex_lock(&_mutex); }
void Unlock() { pthread_mutex_unlock(&_mutex); }
pthread_mutex_t *GetMutexOriginal() { return &_mutex; } // 需要原生接口时用
~Mutex() { pthread_mutex_destroy(&_mutex); }
private:
pthread_mutex_t _mutex;
};
② LockGuard(RAII 风格的锁管理)
class LockGuard {
public:
LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
~LockGuard() { _mutex.Unlock(); }
private:
Mutex &_mutex;
};
用起来业务代码变得极简,且任何出口都不会漏解锁:
Mutex mutex;
while (1) {
LockGuard lockguard(mutex); // 作用域开始 → 加锁
if (ticket > 0) { usleep(1000); printf(...); ticket—; }
else break; // 作用域结束/break → 析构自动解锁
}
RAII 风格的互斥锁 C++11 已内建,对应 std::mutex + std::lock_guard<std::mutex>;本节只是自行封装以便后面(生产者消费者、线程池)直接使用。
网硕互联帮助中心




评论前必须登录!
注册