互斥锁实现与信号传递

11
当互斥锁已被T1锁定,T2试图锁定它时,T2的过程是什么?
我认为大致如下:
-T2尝试锁定,失败,可能会自旋一段时间,然后调用yield…… -T2被执行几次,尝试锁定失败,放弃CPU调度…… -最终T1解锁,T2被调度执行并成功锁定互斥锁……
T1解锁是否明确向调度程序或其他线程发出信号表明互斥锁已解锁?还是只是解锁,并让调度程序在适当时候调度阻塞的线程(即调度程序没有阻塞线程的概念,并且不将它们视为特殊)?

1
我认为互斥原则保证了没有2个或更多的线程同时进入临界区。现在,无论您是否为试图锁定互斥锁的线程设置队列,或者只是忙等待直到互斥锁可用,这取决于您如何实现以及对互斥锁的需求是什么。例如,像VxWorks这样的RTOS中的“mutex”实现了优先级上限协议,而在通用操作系统(GPOS)中可能不需要。 - Raj
3个回答

6
这取决于您的操作系统。我见过只有旋转、旋转与yield、内核中的通用条件变量、用户空间控制的调度和带有内核支持的专用锁原语。
旋转和旋转与yield的性能很差。理论上,用户空间控制的调度(参见Scheduler activations)应该具有最佳性能,但据我所知,没有人在所有情况下都使其正常工作。内核中的通用条件变量和带有内核支持的专用锁原语应该与futex在Linux中表现出类似的性能,后者是最好的例子。

有些情况下,自旋可能会有更好的性能。在Solaris中,内核中的某些锁原语具有自适应模式,其中锁在持有锁的进程在不同的CPU上运行时会自旋。如果锁所有者睡眠或被抢占,则锁等待者也会睡眠。在其他内核中,有一类锁,其中持有锁时无法抢占或睡眠,因此在这些情况下,自旋也很有效。然而,总体而言,特别是在用户空间中,自旋有如此可怕的退化情况(自旋进程一直自旋,直到它被抢占以让锁所有者运行),因此对于性能来说非常糟糕。请注意,像futex这样的专门锁原语可以实现这样的优化,而通用条件变量通常无法实现。


很好的A,但是“使用yield旋转和旋转的性能非常糟糕”应该有所限制...我可以想象在锁定下低争用和低工作量的情况下自旋锁是快速的场景。 - NoSenseEtAl
1
当没有争用时,所有锁都是(相当)高效的。当存在高度争用时,设计良好的锁定方法也同样高效。这还取决于可用处理器的数量——在单处理器系统上,“自旋”等待非常糟糕,因为当前持有锁的另一个线程将无法运行,而T2正在使用所有CPU检查T1是否已释放锁定,这是对CPU时间的无用使用。 - Mats Petersson
@Mats 我在考虑多核系统,但是当然我同意你的单核观点。 - NoSenseEtAl

3
简而言之:是的,也许...
这是实现细节,不知道你指的是哪个操作系统很难说。通常情况下,解锁互斥量只会将等待的线程标记为“可运行”,但不一定会立即调用调度程序。即使调用了调度程序,也不能保证T2会成为下一个要运行的线程。
在Linux中,代码进入mutex_unlock(),它会检查是否有等待任务(通过检查锁计数是否小于零 - 它从1开始解锁,单个锁请求将其变为零,进一步尝试锁定将使其变为负数)。如果有其他等待进程,则调用“慢速路径解锁”,通过几个重定向函数允许实现细节,最终进入__mutex_unlock_common_slowpath - 几行之后,调用wake_up_process,最终进入try_to_wake_up - 它基本上只是将任务排队为“准备运行”,然后调用调度程序(通过多层函数!)

所以你的意思是,在Linux上,阻塞的线程被标记为不可运行(因此在被标记为可运行之前,调度程序不会安排它)。 - NoSenseEtAl
是的,没错。这假设您正在使用内核互斥量,但这可能不是例如pthread_mutex实现的方式。 - Mats Petersson
非常好的回答... 顺便问一下,不打扰你太多... 你能快速说一下futex与此有什么显著区别吗? - NoSenseEtAl
我快速查看了futex代码,它看起来非常不同,在内核中具有更大的功能... - Mats Petersson

1
说我们有以下情景:
 1. T1 got M1. M1 locked.
 2. T2 tries to get M1 and gets blocked as M1 is locked.
 3. T3 tries to get M1 and gets blocked as M1 is locked.
 4. ...some time later...
 5. T1 unlocks M1.*
 6. T2 got M1.
 7. T3 is unblocked and tries to get M1 but is blocked again as T2 got M1 first.

系统调用unlock应该通知所有在互斥锁的锁调用上被阻塞的任务/进程/线程。然后它们被调度执行。这并不意味着它们会被执行,因为可能已经有人在执行了。正如其他人所说,这取决于实现方式。如果你真的想学好这个,我建议你看看这本

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