|
4 | 4 | 本节导读 |
5 | 5 | ----------------------------------------- |
6 | 6 |
|
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>` 的基础上,有的时候我们还需要基于共享资源的状态进行同步。如下面的例子所示: |
9 | 17 |
|
10 | 18 | .. code-block:: rust |
11 | 19 | :linenos: |
12 | 20 |
|
13 | 21 | static mut A: usize = 0; |
14 | 22 | unsafe fn first() -> ! { |
15 | | - A=1; |
| 23 | + A = 1; |
16 | 24 | ... |
17 | 25 | } |
18 | 26 |
|
19 | 27 | unsafe fn second() -> ! { |
20 | | - while A==0 { |
21 | | - // 忙等或睡眠等待 A==1 |
| 28 | + while A == 0 { |
| 29 | + // 忙等直到 A==1 |
22 | 30 | }; |
23 | 31 | //继续执行相关事务 |
24 | 32 | } |
25 | 33 |
|
| 34 | +其中,全局变量 ``A`` 初始值为 0。假设两个线程并发运行,分别执行 ``first`` 和 ``second`` 函数,那么这里的同步需求是第二个线程必须等待第一个线程将 ``A`` 修改成 1 之后再继续执行。 |
26 | 35 |
|
27 | | -在上面的例子中,如果线程second先执行,会忙等在while循环中,在操作系统的调度下,线程first会执行并把A赋值为1后,然后线程second再次执行时,就会跳出while循环,进行接下来的工作。配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示: |
| 36 | +.. 在上面的例子中,如果线程second先执行,会忙等在while循环中,在操作系统的调度下,线程first会执行并把A赋值为1后,然后线程second再次执行时,就会跳出while循环,进行接下来的工作。配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示: |
| 37 | +
|
| 38 | +如何实现这种同步需求呢?首先需要注意到全局变量 ``A`` 是一种共享资源,需要用互斥锁保护它的并发访问: |
28 | 39 |
|
29 | 40 | .. code-block:: rust |
30 | 41 | :linenos: |
31 | 42 |
|
32 | 43 | static mut A: usize = 0; |
| 44 | +
|
33 | 45 | 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); |
37 | 49 | ... |
38 | 50 | } |
39 | 51 |
|
40 | 52 | 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); |
44 | 56 | //继续执行相关事务 |
45 | 57 | } |
46 | 58 |
|
47 | | -这种实现能执行,但效率低下,因为线程second会忙等检查,浪费处理器时间。我们希望有某种方式让线程second休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码: |
| 59 | +然而,这种实现并不正确。假设执行 ``second`` 的线程先拿到锁,那么它需要等到执行 ``first`` 的线程将 ``A`` 改成 1 之后才能退出忙等并释放锁。然而,由于线程 ``second`` 一开始就拿着锁也不会释放,线程 ``first`` 无法拿到锁并修改 ``A`` 。这样,实际上构成了死锁,线程 ``first`` 可能被阻塞,而线程 ``second`` 一直在忙等,两个线程无法做任何有意义的事情。 |
| 60 | + |
| 61 | +为了解决这个问题,我们需要修改 ``second`` 中忙等时锁的使用方式: |
48 | 62 |
|
49 | 63 | .. 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 |
50 | 119 | :linenos: |
51 | 120 |
|
52 | 121 | static mut A: usize = 0; |
|
67 | 136 | //继续执行相关事务 |
68 | 137 | } |
69 | 138 |
|
70 | | -粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程second在睡眠的时候,mutex是否已经上锁了? 确实,线程second是带着上锁的mutex进入等待睡眠状态的。如果这两个线程的调度顺序是先执行线程second,再执行线程first,那么线程second会先睡眠且拥有mutex的锁;当线程first执行时,会由于没有mutex的锁而进入等待锁的睡眠状态。结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。 |
| 139 | +.. 粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程second在睡眠的时候,mutex是否已经上锁了? 确实,线程second是带着上锁的mutex进入等待睡眠状态的。如果这两个线程的调度顺序是先执行线程second,再执行线程first,那么线程second会先睡眠且拥有mutex的锁;当线程first执行时,会由于没有mutex的锁而进入等待锁的睡眠状态。结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。 |
71 | 140 |
|
72 | | -这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)** 这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。 |
| 141 | +.. 这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)** 这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。 |
73 | 142 |
|
74 | 143 | 条件变量的基本思路 |
75 | 144 | ------------------------------------------- |
|
0 commit comments