Skip to content

Commit c89608c

Browse files
committed
update ch8-4
1 parent 250df7e commit c89608c

File tree

2 files changed

+34
-1
lines changed

2 files changed

+34
-1
lines changed

source/chapter8/4condition-variable.rst

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,37 @@
221221
- Hansen 语义:优先级 :math:`T_1>T_2>其他线程` 。即 :math:`T_1` 发现条件满足之后,先继续执行,直到退出管程之前再使用 ``signal`` 唤醒并 **将锁转交** 给 :math:`T_2` ,于是 :math:`T_2` 可以继续执行。注意在 Hansen 语义下, ``signal`` 必须位于管程过程末尾。
222222
- Mesa 语义:优先级 :math:`T_1>T_2=其他线程` 。即 :math:`T_1` 发现条件满足之后,就可以使用 ``signal`` 唤醒 :math:`T_2` ,但是并 **不会将锁转交** 给 :math:`T_2` 。这意味着在 :math:`T_1` 退出管程过程释放锁之后, :math:`T_2` 还需要和其他线程竞争,直到抢到锁之后才能继续执行。
223223

224+
这些优先级顺序如下图所示:
225+
226+
.. image:: condvar-priority.png
227+
:align: center
228+
229+
可以看出, Hoare 和 Hansen 语义的区别在于 :math:`T_1` 和 :math:`T_2` 的优先级顺序不同。 Hoare 语义认为被唤醒的线程应当立即执行,而 Hensen 语义则认为应该优先继续执行当前线程。二者的相同之处在于它们都将锁直接转交给唤醒的线程,也就保证了 :math:`T_2` 一定紧跟着 :math:`T_1` 回到管程过程中,于是在 :math:`T_2` **被唤醒之后其等待的条件一定是成立的** (因为 :math:`T_1` 和 :math:`T_2` 中间没有其他线程),因此 **没有必要重复检查条件是否成立就可以向下执行** 。相对的, Mesa 语义中 :math:`T_1` 就不会将锁转交给 :math:`T_2` ,而是将锁释放让 :math:`T_2` 和其他同优先级的线程竞争。这样, :math:`T_1` 和 :math:`T_2` 之间可能存在其他线程,这些线程的执行会影响到共享资源,以至于 :math:`T_2` 抢到锁继续执行的时候,它所等待的条件又已经不成立了。所以,在 Mesa 语义下, **wait 操作返回之时不见得线程等待的条件一定成立,有必要重复检查确认之后再继续执行** 。
230+
231+
.. note::
232+
233+
**条件等待应该使用 if/else 还是 while?**
234+
235+
在使用 ``wait`` 操作进行条件等待的时候,通常有以下两种方式:
236+
237+
.. code-block:: c
238+
239+
// 第一种方法,基于 if/else
240+
if (!condition) {
241+
wait();
242+
} else {
243+
...
244+
}
245+
246+
// 第二种方法,基于 while
247+
while (!condition) {
248+
wait();
249+
}
250+
251+
如果基于 if/else 的话,其假定了 ``wait`` 返回之后条件一定已经成立,于是不再做检查直接向下执行。而基于 while 循环的话,则是无法确定 ``wait`` 返回之后条件是否成立,于是将 ``wait`` 包裹在一个 while 循环中重复检查直到条件成立。
252+
253+
根据上面的分析可以,如果条件变量是 Mesa 语义,则必须将 ``wait`` 操作放在 while 循环中;如果是 Hoare/Hansen 语义,则使用 if/else 或者 while 均可。在不能确定条件变量为何种语义的情况下,应使用 while 循环,这样保证不会出错。
254+
224255

225256
.. 管程有一个很重要的特性,即任一时刻只能有一个活跃线程调用管程中过程,这一特性使线程在调用执行管程中过程时能保证互斥,这样线程就可以放心地访问共享变量。管程是编程语言的组成部分,编译器知道其特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用,比如编译器可以在管程中的每个过程的入口/出口处加上互斥锁的加锁/释放锁的操作。因为是由编译器而非程序员来生成互斥锁相关的代码,所以出错的可能性要小。
226257
@@ -232,7 +263,9 @@
232263
.. - Hansen语义:是执行唤醒操作的线程必须立即退出管程,即唤醒操作只可能作为一个管程过程的最后一条语句。注:此时唤醒线程的执行位置离开了管程。
233264
.. - Mesa语义:唤醒线程在发出行唤醒操作后继续运行,并且只有它退出管程之后,才允许等待的线程开始运行。注:此时唤醒线程的执行位置还在管程中。
234265
235-
一般开发者会采纳Brinch Hansen的建议,因为它在概念上更简单,并且更容易实现。这种沟通机制的具体实现就是 **条件变量** 和对应的操作:wait和signal。线程使用条件变量来等待一个条件变成真。条件变量其实是一个线程等待队列,当条件不满足时,线程通过执行条件变量的wait操作就可以把自己加入到等待队列中,睡眠等待(waiting)该条件。另外某个线程,当它改变条件为真后,就可以通过条件变量的signal操作来唤醒一个或者多个等待的线程(通过在该条件上发信号),让它们继续执行。
266+
一般情况下条件变量会使用 Hansen 语义,因为它在概念上更简单,并且更容易实现。其实除了条件变量之外,这几种语义也作用于其他基于阻塞-唤醒机制的同步原语。例如,前两节的互斥锁和信号量就是基于 Hansen 语义实现的,有兴趣的同学可以回顾一下。在操作系统中 Mesa 语义也比较常用。
267+
268+
.. 一般开发者会采纳Brinch Hansen的建议,因为它在概念上更简单,并且更容易实现。这种沟通机制的具体实现就是 **条件变量** 和对应的操作:wait和signal。线程使用条件变量来等待一个条件变成真。条件变量其实是一个线程等待队列,当条件不满足时,线程通过执行条件变量的wait操作就可以把自己加入到等待队列中,睡眠等待(waiting)该条件。另外某个线程,当它改变条件为真后,就可以通过条件变量的signal操作来唤醒一个或者多个等待的线程(通过在该条件上发信号),让它们继续执行。
236269
237270
238271
早期提出的管程是基于Concurrent Pascal来设计的,其他语言,如C和Rust等,并没有在语言上支持这种机制。我们还是可以用手动加入互斥锁的方式来代替编译器,就可以在C和Rust的基础上实现原始的管程机制了。在目前的C语言应用开发中,实际上也是这么做的。这样,我们就可以用互斥锁和条件变量来重现实现上述的同步互斥例子:
144 KB
Loading

0 commit comments

Comments
 (0)