# 3.4.1并发系统中时间的本质

> 译文： [https://sicp.comp.nus.edu.sg/chapters/64](https://sicp.comp.nus.edu.sg/chapters/64)

从表面上看，时间似乎很简单。 这是对事件施加的命令。 [[1]](64#footnote-1) 对于任何事件![A](img/efdb05f076173b39fdd26ef663e7b0d8.jpg)和![B](img/813135a6280e2672503128d3d2080d4a.jpg)，![A](img/efdb05f076173b39fdd26ef663e7b0d8.jpg)发生在![B](img/813135a6280e2672503128d3d2080d4a.jpg)，![A](img/efdb05f076173b39fdd26ef663e7b0d8.jpg)和![B](img/813135a6280e2672503128d3d2080d4a.jpg)同时发生，或![A](img/efdb05f076173b39fdd26ef663e7b0d8.jpg)发生在![B](img/813135a6280e2672503128d3d2080d4a.jpg)之后。 例如，回到银行帐户示例，假设Peter从最初包含 &lt;dollar&gt;&lt;/dollar&gt; 100的联名帐户中提取了 &lt;dollar&gt;&lt;/dollar&gt; 10，而Paul提取了 &lt;dollar&gt;&lt;/dollar&gt; 25，剩下 &lt;dollar&gt;&lt;/dollar&gt; 65。 根据两次提款的顺序，帐户中的余额顺序为![%5C](img/c9fb3ce835498ee95f920d1caf849660.jpg) 100 \ rightarrow \ ![90%20%5Crightarrow%5C](img/09fc0da93c1efde37f8719576fca5168.jpg) 65 ![%0A%20%20%20%20or%0A%20%20%20%20%3Cimg%20alt%3D%22%255C%22%20src%3D%22https%3A%2F%2Fwww.zhihu.com%2Fequation%3Ftex%3D%255C%22%20%2F%3E100%20%5Crightarrow%20%5C%3Cimg%20alt%3D%2275%2520%255Crightarrow%255C%22%20src%3D%22https%3A%2F%2Fwww.zhihu.com%2Fequation%3Ftex%3D75%2520%255Crightarrow%255C%22%20%2F%3E65](img/6de683a8f11335b53ae739c62962c608.jpg)。 在银行系统的计算机实现中，可以通过对变量`balance`的连续分配来建模此余额变化顺序。

但是，在复杂的情况下，这种观点可能会成问题。 假设彼得和保罗以及其他人正在通过分布在世界各地的银行机器网络访问同一银行帐户。 帐户中的实际余额顺序将严格取决于访问的详细时间以及机器之间的通信详细信息。

事件顺序的不确定性会在并发系统的设计中造成严重的问题。 例如，假设由Peter和Paul进行的提款是作为共享公共变量`balance`的两个单独的过程实现的，每个过程由 [3.1.1](49) 节中提供的功能指定 ：

```js
function withdraw(amount) {
    if (balance >= amount) {
        balance = balance - amount;
        return balance;
    } else {
        return "Insufficient funds";
    }
}
```

如果两个流程独立运行，则Peter可能会测试余额并尝试提取合法金额。 但是，在彼得检查余额到彼得完成提款之间，保罗可能会提取一些资金，从而使彼得的测试无效。

事情还会变得更糟。 考虑表达

```js
balance = balance - amount;
```

executed as part of each withdrawal process. This consists of three steps: (1) accessing the value of the `balance` variable; (2) computing the new balance; (3) setting `balance` to this new value. If Peter and Paul's withdrawals execute this statement concurrently, then the two withdrawals might interleave the order in which they access `balance` and set it to the new value.

图 [3.29](64#fig_3.29)&lt;/dollar&gt; 100。此后，Peter拥有 &lt;dollar&gt;&lt;/dollar&gt; 10，Paul拥有 &lt;dollar&gt;&lt;/dollar&gt; 25，银行拥有 &lt;dollar&gt;&lt;/dollar&gt; 75\. [[2]](64#footnote-2)

此处说明的一般现象是，多个进程可能共享一个公共状态变量。 使这种情况变得复杂的是，可能有多个进程试图同时操纵共享状态。 对于银行帐户示例，在每次交易期间，每个客户都应该能够像其他客户不存在一样进行操作。 当客户以一种依赖于余额的方式更改余额时，他必须能够假设，在更改时刻之前，余额仍然是他认为的样子。

 <split>## JavaScript和并发

在最初的设计中，JavaScript不允许两个进程同时应用`withdraw`之类的功能。 实际上，该语言的并发模型使用_事件队列_强制执行由_事件_产生的活动的严格顺序执行。 在2000年代初期，多核计算机开始普及，到2010年左右，JavaScript设计人员通过 _Web worker_ 概念引入了并发进程。 截至2019年，大多数Internet浏览器都支持此功能。 按照最初的设想，网络工作者无法共享数据，例如上面的变量`balance`。 但是，最新的 [ECMAScript规范](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf)中包含一个称为`SharedArrayBuffer`的共享数据结构。 使用`SharedArrayBuffer`，可以如上所述编程`withdraw`功能。 [[3]](64#footnote-3)</split> 

## 并发程序的正确行为

上面的示例代表了可以潜入并发程序中的细微错误。 这种复杂性的根源在于在不同过程之间共享的变量分配。 我们已经知道在编写使用赋值的程序时必须小心，因为计算的结果取决于赋值的发生顺序。 [[4]](64#footnote-4)

对于并发流程，我们必须特别注意分配，因为我们可能无法控制不同流程进行的分配顺序。 如果可能同时进行多个此类更改（例如两个存款人访问联名帐户），我们需要某种方式来确保我们的系统正常运行。 例如，在从联名银行账户提款的情况下，我们必须确保节省金钱。 为了使并发程序正常运行，我们可能必须对并发执行施加一些限制。

<figure>![](img_original/ch3-Z-G-31.svg)**[图3.29](64#fig_3.29)** 时序图显示了两次银行提款中事件顺序的交织如何导致错误的最终余额。</figure>

对并发性的一种可能限制是，规定不能同时发生任何两个更改任何共享状态变量的操作。 这是一个非常严格的要求。 对于分布式银行业务，将要求系统设计人员确保一次只能进行一项交易。 这既低效又过于保守。 图 [3.30](64#fig_3.30) 显示了Peter和Paul共享一个银行帐户，其中Paul也有一个私人帐户。 该图说明了从共享帐户中进行的两次提款（Peter一次提款，Paul一次提款）和Paul私人帐户的存款。 [[5]](64#footnote-5)

从共享帐户中的两次取款不能同时进行（因为必须同时访问和更新同一帐户），并且Paul的存款和取款必须不能同时进行（因为既可以访问并更新Paul的钱包中的金额）。 但是，让Paul存入他的私人帐户与Peter从共享帐户中退出同时进行是没有问题的。

<figure>![](img_original/Fig3.30.svg)**[图3.30](64#fig_3.30)** 从Bank1的联名账户和Bank2的私人账户进行的同时存款和取款。</figure>

对并发的较宽松的限制将确保并发系统产生的结果与流程按某种顺序顺序运行的结果相同。 此要求有两个重要方面。 首先，它不需要进程实际上按顺序运行，而只需要产生与相同的结果就好像它们按顺序运行了一样。 对于图 [3.30](64#fig_3.30)正确的&lt;/quote&gt;结果，因为我们只要求该结果与_某些_顺序顺序相同。 例如，假设Peter和Paul的联名帐户以 &lt;dollar&gt;&lt;/dollar&gt; 100开头，而Peter存入 &lt;dollar&gt;&lt;/dollar&gt; 40，而Paul同时提取了帐户中的一半钱。 然后顺序执行可能会导致帐户余额为 &lt;dollar&gt;&lt;/dollar&gt; 70或 &lt;dollar&gt;&lt;/dollar&gt; 90（请参阅练习 [3.38](64#ex_3.38) ）。 [[6]](64#footnote-6)

正确执行并发程序的要求仍然较弱。 用于模拟扩散（例如，物体中的热量流动）的程序可能包含大量过程，每个过程代表一小部分空间，这些过程会同时更新其值。 每个过程都将其值反复更改为其自身值和其邻居值的平均值。 该算法收敛到正确的答案，而与操作的执行顺序无关； 并发使用共享值没有任何限制。

<exercise>Suppose that Peter, Paul, and Mary share a joint bank account that initially contains <dollar></dollar>100\. Concurrently, Peter deposits <dollar></dollar>10, Paul withdraws <dollar></dollar>20, and Mary withdraws half the money in the account, by executing the following commands: <split>| `Peter:` | `balance = balance + 10` |
| `Paul:` | `balance = balance - 20` |
| `Mary:` | `balance = balance - (balance/2)` |</split> 

1.  假定银行系统强制这三个过程以某种顺序顺序运行，请在完成这三笔交易后列出`balance`的所有可能值。
2.  如果系统允许对过程进行交织，还可以产生其他一些值？ 绘制时序图，如图 [3.29](64#fig_3.29) 所示，以解释这些值如何出现。

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_64_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

* * *

[[1]](64#footnote-link-1) To quote some graffiti seen on a Cambridge building wall: <quote>Time is a device that was invented to keep everything from happening at once.</quote>

[[2]](64#footnote-link-2) An even worse failure for this system could occur if the two assignment statements attempt to change the balance simultaneously, in which case the actual data appearing in memory might end up being a random combination of the information being written by the two processes. Most computers have interlocks on the primitive memory-write operations, which protect against such simultaneous access. Even this seemingly simple kind of protection, however, raises implementation challenges in the design of multiprocessing computers, where elaborate _cache-coherence_ protocols are required to ensure that the various processors will maintain a consistent view of memory contents, despite the fact that data may be replicated (<quote>cached</quote>) among the different processors to increase the speed of memory access.

[[3]](64#footnote-link-3) As of 2019, browsers differ in their support for `SharedArrayBuffer` objects.

[[4]](64#footnote-link-4) The factorial program in section <ref name="sec:costs-of-assignment">[3.1.3](51)</ref> illustrates this for a single sequential process.

[[5]](64#footnote-link-5) The columns show the contents of Peter's wallet, the joint account (in Bank1), Paul's wallet, and Paul's private account (in Bank2), before and after each withdrawal (W) and deposit (D). Peter withdraws <dollar></dollar>10 from Bank1; Paul deposits <dollar></dollar>5 in Bank2, then withdraws <dollar></dollar>25 from Bank1.

[[6]](64#footnote-link-6) A more formal way to express this idea is to say that concurrent programs are inherently _nondeterministic_. That is, they are described not by single-valued functions, but by functions whose results are sets of possible values.

