Skip to content

Commit 91e692e

Browse files
committed
Working on ch8-4
1 parent deb7716 commit 91e692e

File tree

2 files changed

+92
-17
lines changed

2 files changed

+92
-17
lines changed

source/chapter8/3semaphore.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,11 @@
188188
信号量的应用
189189
-------------------------------------------
190190

191-
这里给出两个应用:第一个是信号量作为同步原语的简单应用;第二个则是生产者和消费者基于一个有限缓冲进行协作的复杂问题。
191+
这里给出两个应用:第一个是信号量作为同步原语来解决条件同步问题;第二个则是生产者和消费者基于一个有限缓冲进行协作的复杂问题。
192192

193-
信号量作为同步原语的简单应用
193+
.. _link-cond-sync:
194+
195+
条件同步问题
194196
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
195197

196198
来看这样一个例子:
@@ -319,6 +321,10 @@
319321

320322
从这个例子可以看出,信号量的使用可以是非常灵活的。同一个信号量的 P 操作和 V 操作不一定是连续的,甚至可以不在一个线程上。
321323

324+
.. hint::
325+
326+
请同学们思考:能否将二值信号量的 down 和 up 操作放在循环每次迭代的最外层?为什么?
327+
322328
实现信号量
323329
------------------------------------------
324330

source/chapter8/4condition-variable.rst

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,118 @@
44
本节导读
55
-----------------------------------------
66

7-
到目前为止,我们已经了解了操作系统提供的互斥锁和信号量。但在某些情况下,应用程序在使用这两者时需要非常小心,如果使用不当,就会产生效率低下、竞态条件、死锁或者一些不可预测的情况。为了简化编程,避免错误,计算机科学家针对某些情况设计了一种高层的同步互斥原语。具体而言,在有些情况下,线程需要检查某一条件(condition)满足之后,才会继续执行。
8-
我们来看一个例子,有两个线程first和second在运行,线程first会把全局变量 A设置为1,而线程second在 ``! A == 0`` 的条件满足后,才能继续执行,如下面的伪代码所示:
7+
到目前为止,我们已经了解了操作系统提供的互斥锁和信号量两种同步原语。它们可以用来实现各种同步互斥需求,但是它们比较复杂(特别是信号量),对于程序员的要求较高。如果使用不当,就有可能导致效率低下或者产生竞态条件、死锁或一些不可预测的情况。为了简化编程,避免错误,计算机科学家针对某些情况设计了一种抽象层级较高、更易于使用的同步原语,这就是本节要介绍的条件变量机制。
8+
9+
.. 到目前为止,我们已经了解了操作系统提供的互斥锁和信号量。但在某些情况下,应用程序在使用这两者时需要非常小心,如果使用不当,就会产生效率低下、竞态条件、死锁或者一些不可预测的情况。为了简化编程,避免错误,计算机科学家针对某些情况设计了一种高层的同步互斥原语。具体而言,在有些情况下,线程需要检查某一条件(condition)满足之后,才会继续执行。
10+
11+
.. 我们来看一个例子,有两个线程first和second在运行,线程first会把全局变量 A设置为1,而线程second在 ``! A == 0`` 的条件满足后,才能继续执行,如下面的伪代码所示:
12+
13+
条件变量的背景
14+
----------------------------------------------
15+
16+
首先来看我们需要解决的一类一种同步互斥问题。在信号量一节中提到的 :ref:`条件同步问题 <link-cond-sync>` 的基础上,有的时候我们还需要基于共享资源的状态进行同步。如下面的例子所示:
917

1018
.. code-block:: rust
1119
:linenos:
1220
1321
static mut A: usize = 0;
1422
unsafe fn first() -> ! {
15-
A=1;
23+
A = 1;
1624
...
1725
}
1826
1927
unsafe fn second() -> ! {
20-
while A==0 {
21-
// 忙等或睡眠等待 A==1
28+
while A == 0 {
29+
// 忙等直到 A==1
2230
};
2331
//继续执行相关事务
2432
}
2533
34+
其中,全局变量 ``A`` 初始值为 0。假设两个线程并发运行,分别执行 ``first`` 和 ``second`` 函数,那么这里的同步需求是第二个线程必须等待第一个线程将 ``A`` 修改成 1 之后再继续执行。
2635

27-
在上面的例子中,如果线程second先执行,会忙等在while循环中,在操作系统的调度下,线程first会执行并把A赋值为1后,然后线程second再次执行时,就会跳出while循环,进行接下来的工作。配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示:
36+
.. 在上面的例子中,如果线程second先执行,会忙等在while循环中,在操作系统的调度下,线程first会执行并把A赋值为1后,然后线程second再次执行时,就会跳出while循环,进行接下来的工作。配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示:
37+
38+
如何实现这种同步需求呢?首先需要注意到全局变量 ``A`` 是一种共享资源,需要用互斥锁保护它的并发访问:
2839

2940
.. code-block:: rust
3041
:linenos:
3142
3243
static mut A: usize = 0;
44+
3345
unsafe fn first() -> ! {
34-
mutex.lock();
35-
A=1;
36-
mutex.unlock();
46+
mutex_lock(MUTEX_ID);
47+
A = 1;
48+
mutex_unlock(MUTEX_ID);
3749
...
3850
}
3951
4052
unsafe fn second() -> ! {
41-
mutex.lock();
42-
while A==0 { };
43-
mutex.unlock();
53+
mutex_lock(MUTEX_ID);
54+
while A == 0 { }
55+
mutex_unlock(MUTEX_ID);
4456
//继续执行相关事务
4557
}
4658
47-
这种实现能执行,但效率低下,因为线程second会忙等检查,浪费处理器时间。我们希望有某种方式让线程second休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码:
59+
然而,这种实现并不正确。假设执行 ``second`` 的线程先拿到锁,那么它需要等到执行 ``first`` 的线程将 ``A`` 改成 1 之后才能退出忙等并释放锁。然而,由于线程 ``second`` 一开始就拿着锁也不会释放,线程 ``first`` 无法拿到锁并修改 ``A`` 。这样,实际上构成了死锁,线程 ``first`` 可能被阻塞,而线程 ``second`` 一直在忙等,两个线程无法做任何有意义的事情。
60+
61+
为了解决这个问题,我们需要修改 ``second`` 中忙等时锁的使用方式:
4862

4963
.. code-block:: rust
64+
65+
unsafe fn second() -> ! {
66+
loop {
67+
mutex_lock(MUTEX_ID);
68+
if A == 0 {
69+
mutex_unlock(MUTEX_ID);
70+
} else {
71+
mutex_unlock(MUTEX_ID);
72+
break;
73+
}
74+
}
75+
//继续执行相关事务
76+
}
77+
78+
在这种实现中,我们对忙等循环中的每一次对 ``A`` 的读取独立加锁。这样的话,当 ``second`` 线程发现 ``first`` 还没有对 ``A`` 进行修改的时候,就可以先将锁释放让 ``first`` 可以进行修改。这种实现是正确的,但是基于忙等会浪费大量 CPU 资源和产生不必要的上下文切换。于是,我们可以利用基于阻塞机制的信号量进一步进行改造:
79+
80+
.. code-block:: rust
81+
:linenos:
82+
:emphasize-lines: 6,16
83+
84+
// user/src/bin/condsync_sem.rs
85+
86+
unsafe fn first() -> ! {
87+
mutex_lock(MUTEX_ID);
88+
A = 1;
89+
semaphore_up(SEM_ID);
90+
mutex_unlock(MUTEX_ID);
91+
...
92+
}
93+
94+
unsafe fn second() -> ! {
95+
loop {
96+
mutex_lock(MUTEX_ID);
97+
if A == 0 {
98+
mutex_unlock(MUTEX_ID);
99+
semaphore_down(SEM_ID);
100+
} else {
101+
mutex_unlock(MUTEX_ID);
102+
break;
103+
}
104+
}
105+
//继续执行相关事务
106+
}
107+
108+
按照使用信号量解决条件同步问题的通用做法,我们创建一个 :math:`N=0` 的信号量,其 ID 为 ``SEM_ID`` 。在线程 ``first`` 成功修改 ``A`` 之后,进行 ``SEM_ID`` 的 up 操作唤醒线程 ``second`` ;而在线程 ``second`` 发现 ``A`` 为 0,也即线程 ``first`` 还没有完成修改的时候,会进行 ``SEM_ID`` 的 down 操作进入阻塞状态。这样的话,在线程 ``first`` 唤醒它之前,操作系统都不会调度到它。
109+
110+
上面的实现中有一个非常重要的细节:请同学思考, ``second`` 函数中第 15 行解锁和第 16 行信号量的 down 操作可以交换顺序吗?显然是不能的。如果这样做的话,假设 ``second`` 先拿到锁,它发现 ``A`` 为 0 就会进行信号量的 down 操作在拿着锁的情况下进入阻塞。这将会导致什么问题?如果想要线程 ``second`` 被唤醒,就需要线程 ``first`` 修改 ``A`` 并进行信号量 up 操作,然而前提条件是线程 ``first`` 能拿到锁。这是做不到的,因为线程 ``second`` 已经拿着锁进入阻塞状态了,在被唤醒之前都不会将锁释放。于是两个线程都会进入阻塞状态,再一次构成了死锁。可见,这种 **带着锁进入阻塞的情形是我们需要特别小心的** 。
111+
112+
从上面的例子可以看出,互斥锁和信号量能实现很多功能,但是它们对于程序员的要求较高,一旦使用不当就很容易出现难以调试的死锁问题。对于这种比较复杂的同步互斥问题,就可以用本节介绍的条件变量来解决。
113+
114+
.. 然而,这种实现并不正确,假设执行 ``second`` 的线程先拿到锁,那么它会一直忙等在 while 循环中,也不会把锁释放。而执行 ``first`` 的线程始终拿不到锁,也没有办法将 ``A`` 改成 1
115+
116+
.. 这种实现能执行,但效率低下,因为线程second会忙等检查,浪费处理器时间。我们希望有某种方式让线程second休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码:
117+
118+
.. .. code-block:: rust
50119
:linenos:
51120
52121
static mut A: usize = 0;
@@ -67,9 +136,9 @@
67136
//继续执行相关事务
68137
}
69138
70-
粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程second在睡眠的时候,mutex是否已经上锁了? 确实,线程second是带着上锁的mutex进入等待睡眠状态的。如果这两个线程的调度顺序是先执行线程second,再执行线程first,那么线程second会先睡眠且拥有mutex的锁;当线程first执行时,会由于没有mutex的锁而进入等待锁的睡眠状态。结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。
139+
.. 粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程second在睡眠的时候,mutex是否已经上锁了? 确实,线程second是带着上锁的mutex进入等待睡眠状态的。如果这两个线程的调度顺序是先执行线程second,再执行线程first,那么线程second会先睡眠且拥有mutex的锁;当线程first执行时,会由于没有mutex的锁而进入等待锁的睡眠状态。结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。
71140
72-
这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)** 这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。
141+
.. 这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)** 这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。
73142
74143
条件变量的基本思路
75144
-------------------------------------------

0 commit comments

Comments
 (0)