目录
概念
- mutex:用于互斥访问临界区资源
- condition variable:用于避免在等待同步条件由不满足变化为满足时的忙碌等待(busy waiting),忙等会导致浪费处理器资源
- semaphore:用于对共享资源的访问提供同步操作,记录资源的最大可用数量(max_numbers)
Mutex(互斥锁)
通俗理解:可以把mutex当成是一个临界区资源的钥匙,这把钥匙可以有所属权,不同进程想要访问该资源时,本质上是争夺钥匙的所属权,当一个线程争夺到所属权是,它会对该资源上锁,即Lock(mutex);当该线程完成对资源的访问后,它会放弃该钥匙的所属权,即UnLock(mutex)。
因此,Mutex的上锁和解锁需要在同一个线程中完成,使用mutex访问临界区的伪代码如下
// mutex访问临界区伪代码
Lock(&mutex)
do something in critical section...
UnLock(&mutex)
是否可以使用Mutex实现同步?
从直觉上来理解,是存在一定可行性的。示例代码和注释如下:
// 在开始时先将信号量锁住
Lock(&mutex);
void thread_1()
{
do something before thread_2...
// 例如thread_2中的运算依赖与thread_1的运算结果
// 这样就能保证thread_1计算完成后,再执行thread_2
UnLock(&mutex);
}
void thread_2()
{
Lock(&mutex);
do something after thread_1...
}
以上代码,通过一个互斥量mutex实现了thread_1在thread_2之前执行,从而实现了同步。但是,在实际的thread库的实现中,Lock和UnLock在一个线程中必须是成对出现的。
Condition Variable(条件变量)
条件变量本质是一个等待队列 (wait-queue),该队列支持阻塞等待(blocking-wait)和唤醒操作(wakeup)
为了实现同步操作,诞生了条件变量。条件变量常用的模板格式如下:
mutex_lock(&mutex);
// 非常严重的错误:if (!COND)
// 自旋操作
while (!COND) {
wait(&cv, &mutex);
}
assert(COND);
...
broadcast(&cv);
mutex_unlock(&mutex);
// from: 2024 南京大学《操作系统:设计与实现》
其中,COND表示同步条件是否满足,mutex用于保证走出循环后,条件仍然可以满足。wait()所做的操作为将条件变量cv中的等待数+1,同时丢弃当前锁的拥有权并将线程进入睡眠(这一步想要自己实现很难),未来当有一个线程唤醒时,重新获得锁的拥有权并出循环再次进行判断条件是否满足。
为什么一定需要用到while(!COND)?
这一点主要是由于虚假唤醒:即使没有线程发出该条件的信号,线程也可能被唤醒。具体详细原因可参考条件变量虚假唤醒。
Semaphore(信号量)
信号量本质是一个计数器+互斥锁+等待队列(wait-queue)。信号量对于资源可以用整型表示的同步问题中有着极大的便利:1. 信号量本质上是对互斥锁 (mutex)的一种推广 2. 代码实现上没有像条件变量一样复杂的“自旋”操作,更加干净优雅
通俗理解:可以把Semaphore当成一个共享资源的数量(资源计数),当一个线程访问该共享资源时,会先检查是否有多余的资源可供使用,若有,将信号量减一,表示占用一份该资源,在完成资源的访问后,会归还该资源,同时将信号量加一,表示可用的该资源数量加一;若当前没有多余的资源可供使用,则需要等待。(这种方式也被称为“信号量”机制)
Semaphore可以精准指定唤醒指定数目的线程(这些线程因等待资源而被阻塞),只需要释放指定数目的资源(不超过max_numbers)即可
信号量 vs 条件变量
同步方式 | 优点 | 缺点 |
---|---|---|
信号量 | 互斥锁的自然推广;干净、优雅:没有条件变量的 “自旋” | 对于无法用整型表达的同步问题较为困难 |
条件变量 | 万能:适用于任何同步条件 | 代码较为复杂,“不太好用” |
用条件变量实现信号量
使用条件变量实现信号量很容易,如下代码所示:
void P(sem_t *sem) {
hold(&sem->mutex) {
while (!COND)
cond_wait(&sem->cv, &sem->mutex);
sem->count--;
}
}
void V(sem_t *sem) {
hold(&sem->mutex) {
sem->count++;
cond_broadcast(&sem->cv);
}
}
// from: 2024 南京大学《操作系统:设计与实现》
使用信号量实现条件变量
尝试使用信号量实现条件变量,如下代码所示:
void wait(struct condvar *cv, mutex_t *mutex) {
mutex_lock(&cv->lock);
cv->nwait++;
mutex_unlock(&cv->lock);
mutex_unlock(mutex);
P(&cv->sleep);
mutex_lock(mutex);
}
void broadcast(struct condvar *cv) {
mutex_lock(&cv->lock);
for (int i = 0; i < cv->nwait; i++) {
V(&cv->sleep);
}
cv->nwait = 0;
mutex_unlock(&cv->lock);
}
// from: 2024 南京大学《操作系统:设计与实现》
问题随之而来,对于代码mutex_unlock(mutex); P(&cv->sleep);执行顺序只能为以下两种情况:
- 先释放锁,再执行 P。导致问题:释放锁的一瞬间可能与 broadcast 并发
- 先执行 P,再释放锁。导致问题:P(&cv->sleep) 会 “永久睡眠”
可见无论如何实现,都会产生问题,这也就是条件变量底层实现所解决的问题。若想要详细了解具体,可参考Implementing Condition Variables with Semaphores
参考资料
- 南京大学2024年“操作系统:设计与实现”
- 条件变量虚假唤醒
- Conditional Variable vs Semaphore
- Differences between Conditional variables, Mutexes and Locks
- Implementing Condition Variables with Semaphores
如有错误,欢迎指正