[转] mutex, semaphore和spinlock的区别

本文深入探讨了内核锁机制,包括原子操作、自旋锁、互斥锁、信号量等,对比了它们的特点和应用场景,并详细解释了互斥锁与信号量的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

出处:https://blog.youkuaiyun.com/Wilsonboliu/article/details/19190861

本文由该问题引入到内核锁的讨论,归纳如下

为什么需要内核锁?
多核处理器下,会存在多个进程处于内核态的情况,而在内核态下,进程是可以访问所有内核数据的,因此要对共享数据进行保护,即互斥处理

有哪些内核锁机制?
(1)原子操作
atomic_t数据类型,atomic_inc(atomic_t *v)将v加1
原子操作比普通操作效率要低,因此必要时才使用,且不能与普通操作混合使用
如果是单核处理器,则原子操作与普通操作相同
(2)自旋锁
spinlock_t数据类型,spin_lock(&lock)和spin_unlock(&lock)是加锁和解锁
等待解锁的进程将反复检查锁是否释放,而不会进入睡眠状态(忙等待),所以常用于短期保护某段代码
同时,持有自旋锁的进程也不允许睡眠,不然会造成死锁——因为睡眠可能造成持有锁的进程被重新调度,而再次申请自己已持有的锁
如果是单核处理器,则自旋锁定义为空操作,因为简单的关闭中断即可实现互斥
(3)信号量与互斥量
struct semaphore数据类型,down(struct semaphore * sem)和up(struct semaphore * sem)是占用和释放
struct mutex数据类型,mutex_lock(struct mutex *lock)和mutex_unlock(struct mutex *lock)是加锁和解锁
竞争信号量与互斥量时需要进行进程睡眠和唤醒,代价较高,所以不适于短期代码保护,适用于保护较长的临界区

互斥量与信号量的区别?(转载但找不到原文出处)
(1)互斥量用于线程的互斥,信号线用于线程的同步
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
(2)互斥量值只能为0/1,信号量值可以为非负整数
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问
(3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到
 
作者:fleuria
链接:https://www.zhihu.com/question/47704079/answer/136200849

Mutex 相比信号量增加了所有权的概念,一只锁住的 Mutex 只能由给它上锁的线程解开,只有系铃人才能解铃。Mutex 的功能也就因而限制在了构造临界区上。

一元信号量则可以由任一线程解开。这样多出来的一份语义,就是解决读者-写者问题的工具。比如某进程读取磁盘并进入睡眠,等待中断读取盘块结束之后来唤醒它。这就是可以祭出一元信号量的一个情景,而 Mutex 是解决不了的。『信号量』 这个词本身来自火车站的信号灯,其实本来就暗含着一层 『通知』 的含义。

『同步』这个词也可以拆开看,一侧是等待数据的『事件』或者『通知』,一侧是保护数据的 『临界区』。信号量可以满足这两个功能,但是可以注意到两个功能的应用场景还是蛮大的,有 do one thing and do it best 的空间。linux 内核曾将 semaphore 作为同步原语,后面代码变得较难维护,刷了一把 mutex 变简单了不少还变快了,需要『通知』 的场景则替换为了 completion variable。


作者:姚冬
链接:https://www.zhihu.com/question/47704079/answer/135960522

虽然 Mutex和Semaphore 在一定程度上可以互相替代,比如你可以把 值最大为1 的Semaphore当Mutex用,也可以用Mutex+计数器当Semaphore。

但是对于设计理念上还是有不同的,Mutex管理的是资源的使用权,而Semaphore管理的是资源的数量,有那么一点微妙的小区别。

打个比方,在早餐餐厅,大家要喝咖啡。
如果用Mutex的方式,同时只有一个人可以使用咖啡机,他获得了咖啡机的使用权后,开始做咖啡,其他人只能在旁边等着,直到他做好咖啡后,另外一个人才能获得咖啡机的使用权。

如果用Semaphore的模式,服务员会把咖啡做好放到柜台上,谁想喝咖啡就拿走一杯,服务员会不断做咖啡,如果咖啡杯被拿光了,想喝咖啡的人就排队等着。

Mutex管理的是咖啡机的使用权,而Semaphore管理的是做好的咖啡数量。

作者:二律背反
链接:https://www.zhihu.com/question/47704079/answer/135859188

mutex,一句话:保护共享资源。
典型的例子就是买票:
票是共享资源,现在有两个线程同时过来买票。如果你不用mutex在线程里把票锁住,那么就可能出现“把同一张票卖给两个不同的人(线程)”的情况。

一般人不明白semaphore和mutex的区别,根本原因是不知道semaphore的用途。
semaphore的用途,一句话:调度线程
有的人用semaphore也可以把上面例子中的票“保护"起来以防止共享资源冲突,必须承认这是可行的,但是semaphore不是让你用来做这个的;如果你要做这件事,请用mutex。

在网上、包括stackoverflow等著名论坛上,有一个流传很广的厕所例子:
mutex是一个厕所一把钥匙,谁抢上钥匙谁用厕所,谁没抢上谁就等着;semaphore是多个同样厕所多把同样的钥匙 ---- 只要你能拿到一把钥匙,你就可以随便找一个空着的厕所进去。
事实上,这个例子对初学者、特别是刚刚学过mutex的初学者来说非常糟糕 ----- 我第一次读到这个例子的第一反应是:semaphore是线程池???所以,请务必忘记这个例子
另外,有人也会说:mutex就是semaphore的value等于1的情况。
这句话不能说不对,但是对于初学者来说,请先把这句话视为错误;等你将来彻底融会贯通这部分知识了,你才能真正理解上面这句话到底是什么意思。总之请务必记住:mutex干的活儿和semaphore干的活儿不要混起来。


在这里,我模拟一个最典型的使用semaphore的场景:
a源自一个线程,b源自另一个线程,计算c = a + b也是一个线程。(即一共三个线程)

显然,第三个线程必须等第一、二个线程执行完毕它才能执行。
在这个时候,我们就需要 调度线程了:让第一、二个线程执行完毕后,再执行第三个线程。
此时,就需要用semaphore了。
int a, b, c;
void geta()
{
    a = calculatea();
    semaphore_increase();
}

void getb()
{
    b = calculateb();
    semaphore_increase();
}


void getc()
{
    semaphore_decrease();
    semaphore_decrease();
    c = a + b;
}

t1 = thread_create(geta);
t2 = thread_create(getb);
t3 = thread_create(getc);
thread_join(t3);

这就是semaphore最典型的用法。
说白了,调度线程,就是:一些线程生产(increase)同时另一些线程消费(decrease),semaphore可以让生产和消费保持合乎逻辑的执行顺序。
而线程池是程序员根据具体的硬件水平和不同的设计需求、为了达到最佳的运行效果而避免反复新建和释放线程同时对同一时刻启动的线程数量的限制,这完全是两码事。
比如如果你要计算z = a + b +...+ x + y ...的结果,同时每个加数都是一个线程,那么计算z的线程和每个加数的线程之间的逻辑顺序是通过semaphore来调度的;而至于你运行该程序的时候到底要允许最多同时启动几个线程,则是用线程池来实现的。

请回头看那个让大家忘记的厕所例子。我之所以让大家忘记这个例子,是因为如果你从这个角度去学习semaphore的话,一定会和mutex混为一谈。semaphore的本质就是调度线程 ---- 在充分理解了这个概念后,我们再看这个例子。

semaphore是通过一个值来实现线程的调度的,因此借助这种机制,我们也可以实现对线程数量的限制。例子我就不写了,如果你看懂了上面的c = a + b的例子,相信你可以轻松写出来用semaphore限制线程数量的例子。而当我们把线程数量限制为1时,你会发现:共享资源受到了保护 ------ 任意时刻只有一个线程在运行,因此共享资源当然等效于受到了保护。但是我要再提醒一下,如果你要对共享资源进行保护,请用mutex;到底应该用条件锁还是用semaphore,请务必想清楚。通过semaphore来实现对共享资源的保护的确可行但是是对semaphore的一种错用

只要你能搞清楚锁、条件锁和semaphore为什么而生、或者说它们是面对什么样的设计需求、为了解决什么样类型的问题才出现的,你自然就不会把他们混淆起来。

semaphore和条件锁的区别:
条件锁,本质上还是锁,它的用途,还是围绕“共享资源”的。条件锁最典型的用途就是:防止不停地循环去判断一个共享资源是否满足某个条件。
比如还是买票的例子:
我们除了买票的线程外,现在再加一个线程:如果票数等于零,那么就要挂出“票已售完”的牌子。这种情况下如果没有条件锁,我们就不得不在“挂牌子”这个线程里不断地lock和unlock而在大多数情况下票数总是不等于零,这样的结果就是:占用了很多CPU资源但是大多数时候什么都没做。
另外,假如我们还有一个线程,是在票数等于零时向上级部门申请新的票。同理,问题和上面的一样。而如果有了条件锁,我们就可以避免这种问题,而且还可以一次性地通知所有被条件锁锁住的线程。
这里有个问题,是关于条件锁的:pthread_cond_wait 为什么需要传递 mutex 参数?
不清楚条件锁的朋友可以看一下。
总之请记住:条件锁,是为了避免绝大多数情况下都是lock ---> 判断条件 ----> unlock的这种很占资源但又不干什么事情的线程。它和semaphore的用途是不同的。


简而言之,锁是服务于共享资源的;而semaphore是服务于多个线程间的执行的逻辑顺序的。



[转]mutex和spin lock的区别
mutex和spin lock的区别和应用(sleep-waiting和busy-waiting的区别)2011-10-19 11:43

信号量mutex是sleep-waiting。 就是说当没有获得mutex时,会有上下文切换,将自己、加到忙等待队列中,直到另外一个线程释放mutex并唤醒它,而这时CPU是空闲的,可以调度别的任务处理。

而自旋锁spin lock是busy-waiting。就是说当没有可用的锁时,就一直忙等待并不停的进行锁请求,直到得到这个锁为止。这个过程中cpu始终处于忙状态,不能做别的任务。

例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0 和Core1上。 用spin-lock,coer0上的线程就会始终占用CPU。
另外一个值得注意的细节是spin lock耗费了更多的user time。这就是因为两个线程分别运行在两个核上,大部分时间只有一个线程能拿到锁,所以另一个线程就一直在它运行的core上进行忙等待,CPU占用率一直是100%;而mutex则不同,当对锁的请求失败后上下文切换就会发生,这样就能空出一个核来进行别的运算任务了。(其实这种上下文切换对已经拿着锁的那个线程性能也是有影响的,因为当该线程释放该锁时它需要通知操作系统去唤醒那些被阻塞的线程,这也是额外的开销)

总结
(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。

(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。

(3)更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。

 

总结:

互斥锁支持且只支持互斥访问资源,且只有加锁人才能解锁。即互斥量只支持互斥机制;

信号量支持互斥访问资源,加锁者和解锁者可以是不同人,因此可以实现生产者+消费者模式,可以使进程/线程间有序生产/消费资源。即信号量用于实现进程/线程间同步;

自旋锁与二者的区别就是不能阻塞,一直等待获得锁;


互斥:

是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

某些资源只能被独占使用,或某些关键数据只能被一个进程修改,此时需要互斥机制。

设备有可能被多个用户使用,驱动程序就有可能被多个进程调用,因此如果有资源可能在多个进程间被共享,

比如设备的配置空间对应的内存就有可能被多个设备同时访问,例如nvme设备在被多个用户进程写入时,

io queue的head tail指针会随时更新,如果没有锁机制保证互斥访问,就可能会造成更新错误。

因此需要同步机制保证资源的一致性,避免同时访问造成冲突。

机制:
互斥锁Mutex lock/unlock机制,自旋锁spin lock/unlock机制,原子操作atomic_t机制。


同步:

是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

本质上就是进程间通信,A进程干完某个活之后告诉B进程可以开始干了,或者A进程释放了B进程需要的资源。
机制:

信号量semaphore donw/up机制,completion机制,wait event queue机制。

当资源需要独占使用且只有拥有者能释放时,使用互斥锁机制对资源进行保护和使用;

当资源数量大于1,且可以多个使用者同时使用,则可以使用信号量机制对资源进行分配和使用;

当资源是生产者+消费者模式时,适合使用信号量机制由不同的进程/线程生产资源和使用资源。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值