Mutex(互斥锁)、Semaphore(信号量)、Condition Variable(条件变量)区别

本文详细解释了互斥锁Mutex、条件变量ConditionVariable以及信号量Semaphore在并发编程中的作用,讨论了它们如何实现同步,以及为何条件变量需要while(!COND)循环。同时,对比了它们的优缺点和使用场景,指出条件变量的底层实现解决了特定同步问题中的挑战。
摘要由CSDN通过智能技术生成

概念

  • 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);执行顺序只能为以下两种情况:

  1. 先释放锁,再执行 P。导致问题:释放锁的一瞬间可能与 broadcast 并发
  2. 先执行 P,再释放锁。导致问题:P(&cv->sleep) 会 “永久睡眠”

可见无论如何实现,都会产生问题,这也就是条件变量底层实现所解决的问题。若想要详细了解具体,可参考Implementing Condition Variables with Semaphores

参考资料

如有错误,欢迎指正

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值