什么时候应该使用互斥锁(mutex),什么时候应该使用信号量(semaphore)?

148

我们何时应该使用互斥锁(mutex),何时应该使用信号量(semaphore)?


2
可能是Java中的互斥锁和信号量是什么?主要区别是什么?的重复问题。 - Merlyn Morgan-Graham
13个回答

131

这是我记住何时使用什么的方法 -

信号量(Semaphore): 当您(线程)想要睡眠直到某个其他线程告诉您醒来时,请使用信号量。信号量“down”在一个线程(生产者)中发生,信号量“up”(对同一信号量)在另一个线程(消费者)中发生。 例如:在生产者-消费者问题中,生产者希望睡眠直到至少有一个缓冲槽为空 - 只有消费者线程可以告知何时缓冲槽为空。

互斥锁(Mutex): 当您(线程)想要执行不应由任何其他线程同时执行的代码时,请使用互斥锁。互斥锁“down”在一个线程中发生,而互斥锁“up”必须稍后在同一线程中发生。 例如:如果您正在从全局链表中删除节点,则不希望另一个线程在删除节点时操纵指针。当您获取互斥锁并忙于删除节点时,如果另一个线程尝试获取相同的互斥锁,则该线程将被置于睡眠状态,直到您释放互斥锁。

自旋锁(Spinlock): 当您确实希望使用互斥锁但是您的线程不允许睡眠时,请使用自旋锁。 例如:在OS内核中的中断处理程序绝不能睡眠。如果它这样做,系统将冻结/崩溃。如果您需要从中断处理程序插入节点到全局共享链表中,请获取自旋锁-插入节点-释放自旋锁。


4
添加:信号量和互斥锁都是提供同步的两种方法。 信号量更多与信号相关(例如生产者和消费者问题场景),而互斥锁则更多与一次只允许访问一个有关(多个请求访问共享资源,但每次只允许一个)。 [好文章:http://www.geeksforgeeks.org/mutex-vs-semaphore/] - parasrish
1
只有一个可能的小问题,互斥锁确实保护资源,而不是代码。对这些资源的访问可能在广泛分散的代码部分中执行,因此,只要所有这些部分使用相同的互斥锁,一切都应该没问题。你的回答(对我来说)的方式是,互斥锁仅保护一个代码部分。 - paxdiablo
对于最后一个,我会说自旋锁:内核开发者?不是的话就算了。 - Hunter Kohler

62

互斥量是一个互斥对象,类似于信号量,但它只允许一次加锁,并且其所有权限制可能比信号量更严格。

可以将其视为等效于普通计数信号量(计数为1),并要求只能由锁定它的同一线程释放(a)

另一方面,信号量具有任意数量的计数,并且可以同时被那么多个锁定者锁定。并且它可能没有要求由声称它的同一线程释放(但如果没有,则必须仔细跟踪当前谁有责任,就像分配的内存一样)。

因此,如果您有多个资源实例(例如三个磁带驱动器),则可以使用计数为3的信号量。请注意,这并不告诉您哪些磁带驱动器属于您,只是告诉您有一定数量。

此外,使用信号量,单个锁定者可以锁定多个资源实例,例如进行磁带到磁带的复制。如果您有一个资源(例如不想破坏的内存位置),则互斥量更合适。

等效操作包括:

Counting semaphore          Mutual exclusion semaphore
--------------------------  --------------------------
  Claim/decrease (P)                  Lock
  Release/increase (V)                Unlock

顺带一提:如果你曾经对用于声明和释放信号量的奇怪字母(PV)感到困惑,那是因为发明者是荷兰人。 在荷兰语中:

  • Probeer te verlagen: 意为尝试降低;
  • Verhogen: 意为增加。

(a)......或者可以将其视为与信号量完全不同的东西,这可能会更安全,考虑到它们几乎总是用途不同。


1
从概念上讲,二进制信号量是互斥锁,并且它相当于一个计数为一的普通信号量。在实现上可能存在差异,例如效率或资源所有权(可以由其他人释放,我不同意 - 资源只应该由声明它的线程释放)。 - paxdiablo
1
另一个可能的实现差异是递归互斥锁。由于只有一个资源,单个线程可以多次锁定它(只要它释放相同次数)。这在多实例资源中并不容易,因为您可能不知道线程是想要声明“另一个”实例还是再次声明“相同”的实例。 - paxdiablo
1
它们解决特定的问题。它们解决的问题是那些不太理解互斥量的人,这一点绝不会贬低解决方案 :-) - paxdiablo
6
互斥锁(Mutex)与二进制信号量(Binary Semaphore)完全不同。抱歉,但这个定义是错误的。 - Peer Stritzinger
1
@ToolmakerSteve,我不确定你是否理解我的意图。我说一个mutex就像一个计数为一且要求申请线程必须是释放线程的信号量。我并没有争辩semaphore有这个限制。我会尝试整理答案以更好地区分它们。 - paxdiablo
显示剩余11条评论

60
非常重要的是要明白,互斥锁不是计数为1的信号量!这就是为什么会有像二元信号量这样的东西(它们实际上是计数为1的信号量)。
互斥锁和二元信号量之间的区别在于所有权原则:互斥锁由任务获取,因此也必须由同一任务释放。这使得可以解决二元信号量的几个问题(意外释放、递归死锁和优先级反转)。
注意:我写了“使其成为可能”,如何解决这些问题取决于操作系统的实现。
由于互斥锁必须由同一任务释放,因此不太适合用于任务同步。但是,如果与条件变量结合使用,您将获得非常强大的构建块,可用于构建各种IPC原语。
因此,我的建议是:如果您拥有干净的互斥锁和条件变量(例如POSIX pthreads),请使用这些。
只有在它们完全适合您要解决的问题时才使用信号量,不要尝试构建其他原语(例如使用信号量构建读写锁,而应该使用互斥锁和条件变量)。
互斥锁和信号量之间存在很多误解。到目前为止,我发现的最好的解释是在这篇三部分文章中:互斥锁与信号量-第1部分:信号量

互斥锁 vs. 信号量 - 第2部分:互斥锁

互斥锁 vs. 信号量 - 第3部分(最终部分):互斥问题


该网站的URL包含奇怪的字符,因此无法使用......我正在解决这个问题。 - Peer Stritzinger
1
这些链接已经失效了。这个答案没有解释二元信号量和互斥锁的规范区别。 “所有权原则”是关于同步原语如何使用的,因此它不属于一个规范。我要负评。 - beroal
2
@beroal 我已经编辑过了,链接也更新了。等待更新被接受后,就可以愉快地阅读它们了... - Mohammad Kholghi
对于第三部分,我不明白为什么互斥锁不会发生循环死锁。如果SI任务执行“mutex_lock(DAC)”,然后RTOS切换到Control任务,那么循环死锁不是会出现吗? - ipStack
@beroal 互斥锁的所有权方面不仅仅是使用问题。只有获取互斥锁的任务才能释放它。对于递归互斥锁,如果一个任务获取了一个信号量并调用了一个获取相同信号量的例程,那么它会得到它,因为该任务已经获取了它。如果另一个任务调用相同的例程来获取相同的信号量,它将失败,因为第一个任务仍然拥有它。 - Jeff Learman

15

虽然 @opaxdiablo 的回答完全正确,但我想指出两者的使用场景是非常不同的。互斥锁用于保护代码的某些部分免于并发运行,而信号量用于一个线程向另一个线程发送信号以运行。

/* Task 1 */
pthread_mutex_lock(mutex_thing);
    // Safely use shared resource
pthread_mutex_unlock(mutex_thing);



/* Task 2 */
pthread_mutex_lock(mutex_thing);
   // Safely use shared resource
pthread_mutex_unlock(mutex_thing); // unlock mutex

信号量的情境有所不同:

/* Task 1 - Producer */
sema_post(&sem);   // Send the signal

/* Task 2 - Consumer */
sema_wait(&sem);   // Wait for signal

请参考http://www.netrino.com/node/202以获取更详细的解释。


4
没错,即使你使用信号量并将其计数为1,你仍然会对你所做的事情做出某种暗示,这与使用互斥锁不同。 - Omnifarious
1
虽然我不确定我完全同意这个观点,但我也没那么强烈地反对以至于会给你点踩 :-) 你说信号量的使用模式是通知线程,但当有另一个线程在等待时,互斥锁也正是这样做的;而当sema_wait函数中没有线程时,信号量却不会做这个动作 :-) 在我看来,它们两者都与资源有关,而通知其他线程则是保护的副作用(从性能角度来看非常重要)。 - paxdiablo
你说信号量的使用模式是通知线程的。在通知线程方面有一点需要注意。根据(http://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_04.html),你可以安全地从一个信号处理程序中调用`sem_post`,但不建议从一个信号处理程序中调用`pthread_mutex_lock`和`pthread_mutex_unlock`(http://manpages.ubuntu.com/manpages/lucid/man3/pthread_mutex_init.3.html#contenttoc4)。 - user184968
@paxdiablo:这里有一个主要区别,即互斥锁和二进制信号量之间维护引用计数的方式不同。互斥锁或者可以说任何条件互斥锁都不会维护与锁相关的计数,而信号量则会维护计数。因此,sem_wait和sem_post会维护计数。 - Prak
强调:“互斥锁用于保护代码的某些部分不被并发运行,信号量用于一个线程向另一个线程发出信号以运行。” - John

14

参见“厕所例子” - http://pheatt.emporia.edu/courses/2010/cs557f10/hand07/Mutex%20vs_%20Semaphore.htm

Mutex(互斥量):

是一把厕所的钥匙。一次只能有一个人拥有这把钥匙,使用厕所。当使用完之后,持有者将钥匙交给排队中的下一个人。

正式定义:“互斥锁通常用于串行访问重入代码的区域,该代码不能被多个线程同时执行。互斥锁对象仅允许一个线程进入受控区域,迫使试图访问该区域的其他线程等待,直到第一个线程从该区域退出。” 参考文献:Symbian Developer Library

(实际上,互斥锁就是值为1的信号量。)

Semaphore(信号量):

是相同厕所钥匙的数量。例如,假设我们有四个带有相同锁和钥匙的厕所。在开始时,信号量计数 - 钥匙计数 - 设置为4(所有四个厕所都是空闲的),然后随着人们进入,计数值递减。如果所有厕所都满了,即没有空闲的钥匙了,则信号量计数为0。现在,当一个人离开厕所时,信号量增加1(一个自由的钥匙),并交给排队中的下一个人。

正式定义:“信号量限制共享资源的最大同时使用者数量。线程可以请求访问该资源(递减信号量),并且可以发出已经完成对该资源的使用的信号(递增信号量)。” 参考文献:Symbian Developer Library


请注意还有另一个区别。首先,您可以拥有二进制信号量(相当于最大计数为1的计数信号量)。因此,密钥数量不是唯一的区别。其次,只有获得互斥锁的人才能将其返回。对于信号量,任何人都可以返回密钥,即使是没有“拥有”它的人。此外,互斥锁通常是“递归”的,因此如果任务获取信号量并调用获取相同信号量的函数,则成功,因为该任务具有互斥锁。如果另一个任务同时调用该函数,则等待,因为另一个任务具有互斥锁。 - Jeff Learman

7

Mutex是用来保护共享资源的。
Semaphore是用来调度线程的。

Mutex:
假设有一些门票要出售。我们可以模拟这样一个场景:许多人同时购买门票,每个人都是一个购票的线程。显然,我们需要使用Mutex来保护门票,因为它是共享资源。


Semaphore:
假设我们需要进行以下计算:

c = a + b;

此外,我们需要一个函数geta()来计算a,一个函数getb()来计算b,以及一个函数getc()来计算c = a + b
显然,除非geta()getb()已经完成,否则我们无法执行c = a + b
如果这三个函数是三个线程,我们需要分派这三个线程。
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);

借助信号量,上述代码可以确保在 t1 和 t2 完成任务之前,t3 不会执行其任务。
简而言之,信号量是使线程按照逻辑顺序执行的工具,而互斥锁是用于保护共享资源的工具。
因此,尽管有些人总是说互斥锁是具有初始值为 1 的特殊信号量,但它们不是同一物品。你也可以这样说,但请注意它们的使用情况不同。即使你可以这样做,请不要将一个工具替换为另一个工具。

售票是一个很好的例子。信号量示例有点不清楚(至少对我来说是这样)。 - prayagupa
1
Semaphore的例子是为了让线程按照某种顺序进行,而售票不需要任何顺序。如果有三个人:a、b和c。当他们来买票时,我们根本不关心购买车票的顺序。但是,如果我们做这样的计算:x = getx(); y = gety(); z = x + y;出于某种原因,我们使用三个线程来完成这三件事,现在线程的顺序非常重要,因为除非getxgety已经完成,否则我们无法执行x + y。总之,当我们关心多线程执行的顺序时,会使用信号量。 - Yves
我明白了。这听起来类似于屏障。我可以说等待线程xy完成,然后计算z = x + y。我知道Java有CyclicBarrier。此外,我不确定是否可以说mapreduce也是信号量用例,因为在所有map完成之前我无法进行reduce - prayagupa
@prayagupd 是的。你可以这么说。 - Yves

6

我认为问题应该是mutex和二进制信号量之间的区别。

Mutex = 它是一种所有权锁机制,只有获取锁的线程才能释放锁。

二进制信号量 = 它更像是一个信号机制,任何其他更高优先级的线程如果想要可以发信号并获取锁。


6
尽量不要听起来怪异,但我控制不了自己。
你的问题应该是互斥锁和信号量之间有什么区别? 更精确地说,问题应该是“互斥锁和信号量之间的关系是什么?”
(我本来会加上这个问题,但我百分之百确定一些过度热衷的版主会将其关闭为重复项,而不理解区别和关系。)
在对象术语中,我们可以观察到:
观察1. 信号量包含互斥锁
观察2. 互斥锁不是信号量,信号量也不是互斥锁。
有一些信号量会表现得像互斥锁一样,称为二进制信号量,但它们并不是互斥锁。
有一个特殊的成分叫做信号(posix使用condition_variable作为名称),需要将互斥锁转换为信号量。将其视为通知源。如果两个或多个线程订阅了同一通知源,则可能向它们发送消息以唤醒其中一个或所有人。
信号量可能有一个或多个由互斥锁保护的计数器。对于信号量的最简单的情况是,有一个只能为0或1的单个计数器。
这就是混淆如季风雨般倾泻的地方。
计数器可以为0或1的信号量不是互斥锁。
互斥锁有两种状态(0,1)和一个所有权(任务)。 信号量具有互斥锁、一些计数器和条件变量。
现在,使用您的想象力,每个计数器的使用组合以及何时发出信号都可能产生一种信号量。
现在回答你的问题,什么时候使用什么。(或者更正问题版本3,什么时候使用互斥锁,什么时候使用二进制信号量,因为没有与非二进制信号量进行比较。) 当您需要自定义行为时,请使用互斥锁,该行为不是由二进制信号量提供的,例如自旋锁或快速锁或递归锁。通常可以使用属性自定义互斥锁,但自定义信号量只是编写新信号量而已。 当您需要轻量级或更快的基元时,请使用互斥锁。
当您需要的正是信号量所提供的内容时,请使用信号量。
如果您不理解二进制信号量的实现提供了什么,则在我看来,请使用互斥锁。
最后,请阅读一本书,而不是仅依赖SO。

2
所有上述答案都是很好的质量,但这个答案只是为了记忆。名称Mutex源自Mutually Exclusive,因此你会想到互斥锁是两者之间的互斥,即同一时间只有一个人能够使用它,如果我拥有它,你只能在我释放后才能使用它。另一方面,对于Semaphore来说,这种情况并不存在,它就像一个交通信号灯(Semaphore这个词也是这个意思)。

1

正如所指出的,计数为1的信号量与“二进制”信号量以及互斥锁是相同的。

我看到使用计数大于1的信号量的主要情况是在生产者/消费者的情况下,你有一个固定大小的队列。

然后你需要两个信号量。第一个信号量最初设置为队列中的项目数量,第二个信号量设置为0。生产者对第一个信号量执行P操作,将其添加到队列中,并对第二个信号量执行V操作。消费者对第二个信号量执行P操作,从队列中删除,然后对第一个信号量执行V操作。

通过这种方式,当生产者填满队列时,它会被阻塞,而当队列为空时,消费者会被阻塞。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接